<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>XiaoHanys</title>
  
  <subtitle>Don&#39;t forget to be awesome!</subtitle>
  <link href="https://blog.xiaohanys.top/atom.xml" rel="self"/>
  
  <link href="https://blog.xiaohanys.top/"/>
  <updated>2026-03-26T09:21:36.509Z</updated>
  <id>https://blog.xiaohanys.top/</id>
  
  <author>
    <name>XiaoGuang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>NextTV-我自己写了一个在线影视站</title>
    <link href="https://blog.xiaohanys.top/my-next-tv/"/>
    <id>https://blog.xiaohanys.top/my-next-tv/</id>
    <published>2026-01-17T09:23:07.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>AI时代真的给人带来了无限可能，之前不敢想的事情现在都可以做了。个人对 LunaTV 和 LibreTV 都不甚满意，<emp>LunaTV 功能太复杂了，而 LibreTV</emp>又太简单，页面太赛博朋克了，不是很喜欢。于是借助 AI，我自己实现了我的个人影视站。</p><span id="more"></span><h2 id="为什么要自己再做一个"><a href="#为什么要自己再做一个" class="headerlink" title="为什么要自己再做一个"></a>为什么要自己再做一个</h2><p>其实很早之前我就自己搭建过影视站点，只不过用的都是传统的基于PhP的CMS模板，例如海洋CMS。</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="海洋CMS" href="https://www.seacms.net/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">海洋CMS</span><span class="cap link footnote">https://www.seacms.net/</span></div><div class="right"><div class="lazy img" data-bg="https://tncache1-f1.v3mh.com/image/2026/01/18/7aa0a54b4515eae52415c1761d42429f.png"></div></div></a></div><p>但是用户体验很不好，<emp>管理员需要在后台把资源采集到数据库里，这个过程非常的慢；另一方面, 像海洋CMS这种网站的前端模板都很不好看</emp>，要么就要付费定制，于是不如从头自己做。</p><p><a href="https://github.com/LibreSpark/LibreTV">LibreTV</a> 和 <a href="https://github.com/MoonTechLab/LunaTV">LunaTV</a> 事实上和传统的 CMS 站点不太一样，其本地不需要保存采集源，而是直接<emp>调用采集源提供的接口来搜索和获取数据</emp>，然后动态的展示出来，这也得益于前端技术的快速发展。</p><h3 id="LibreTV太简单"><a href="#LibreTV太简单" class="headerlink" title="LibreTV太简单"></a>LibreTV太简单</h3><p>我不喜欢LibreTV的地方在于它的赛博朋克风格实在难以适应，个人更喜欢小清新和暖色调的网站，另一方面，LibreTV没有我想要的弹幕功能，而且早已经不再维护了。</p><h3 id="LunaTV太复杂"><a href="#LunaTV太复杂" class="headerlink" title="LunaTV太复杂"></a>LunaTV太复杂</h3><p>LunaTV的功能太多了，就像一个百宝箱一样想要囊括万物，但是我只需要一个看剧的平台，这么多功能其实没必要。</p><h2 id="我实现了哪些功能"><a href="#我实现了哪些功能" class="headerlink" title="我实现了哪些功能"></a>我实现了哪些功能</h2><p>我实现了我认为一个影视站应该具备的最最基础的功能，例如：</p><ul><li><strong>豆瓣推荐</strong> - 首页展示热门和高分影视内容</li><li><strong>红果短剧推荐</strong> - 首页展示热门和高分短剧内容</li><li><strong>多源视频搜索</strong> - 支持自定义多个视频源 API，聚合搜索电影和电视剧</li><li><strong>预测速</strong> - 对搜索到的视频预测速和延迟，帮助用户选择最佳视频</li><li><strong>高级播放器</strong> - 基于 Artplayer，支持 HLS&#x2F;M3U8 流媒体播放</li><li><strong>快捷键支持</strong> - 丰富的键盘快捷键，提升观看体验</li><li><strong>弹幕系统</strong> - 手动设置接口，实时抓取最新官网弹幕</li><li><strong>去广告功能(实验功能)</strong> - 并不总是生效，因为采集站的广告片段特征越来越少</li><li><strong>片头片尾跳过</strong> - 自动跳过片头片尾，可自定义跳过时间点</li><li><strong>播放历史</strong> - 自动保存观看进度，随时继续观看</li><li><strong>收藏管理</strong> - 收藏喜爱的视频，方便快速访问</li><li><strong>灵活配置</strong> - 可视化管理视频源和弹幕源，支持导入导出</li><li><strong>直链播放</strong> - 直接播放OpenList的链接，畅享高清+弹幕体验</li></ul><p>网站截图：</p><div class="tag-plugin gallery grid-box" size="mix" ratio="square"><div class="grid-cell lazy-box"><img class="lazy" data-fancybox="gallery-1" data-src="https://tncache1-f1.v3mh.com/image/2026/01/16/3c7155e313df3bdae29b66815a42b3db.png" alt="@首页"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div><div class="image-meta"><span class="image-caption">@首页</span></div></div><div class="grid-cell lazy-box"><img class="lazy" data-fancybox="gallery-1" data-src="https://tncache1-f1.v3mh.com/image/2026/01/18/b9f62485e73393e9c629e7ca188c2a58.png" alt="@搜索页"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div><div class="image-meta"><span class="image-caption">@搜索页</span></div></div><div class="grid-cell lazy-box"><img class="lazy" data-fancybox="gallery-1" data-src="https://tncache1-f1.v3mh.com/image/2026/01/16/fc1aaa5124285bf4d02fc8df8193821c.png" alt="@设置页"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div><div class="image-meta"><span class="image-caption">@设置页</span></div></div><div class="grid-cell lazy-box"><img class="lazy" data-fancybox="gallery-1" data-src="https://tncache1-f1.v3mh.com/image/2026/01/18/794be0d441001a1d79e64188552df948.png" alt="@播放页"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div><div class="image-meta"><span class="image-caption">@播放页</span></div></div></div><h2 id="如何部署"><a href="#如何部署" class="headerlink" title="如何部署"></a>如何部署</h2><h3 id="无服务器部署"><a href="#无服务器部署" class="headerlink" title="无服务器部署"></a>无服务器部署</h3><p><a href="https://vercel.com/">Vercel</a> 和 <a href="https://edgeone.ai/">EdgeOnePage</a> 都可以很好的兼容Next.js的项目，所以Fork本仓库，然后一键部署即可，无需额外复杂的环境变量</p><h3 id="Docker本地部署"><a href="#Docker本地部署" class="headerlink" title="Docker本地部署"></a>Docker本地部署</h3><p>也很简单，一行命令即可</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --name nexttv -p 3000:3000 ghcr.io/seqcrafter/nexttv:latest</span><br></pre></td></tr></table></figure><p>然后就可以在<code>http://localhost:3000</code>观看了。</p><h2 id="弹幕功能如何开启"><a href="#弹幕功能如何开启" class="headerlink" title="弹幕功能如何开启"></a>弹幕功能如何开启</h2><p><strong>LogVar:</strong></p><div class="tag-plugin link dis-select"><a class="link-card plain" title="LogVar" href="https://github.com/SeqCrafter/danmu_api" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">LogVar</span><span class="cap link footnote">https://github.com/SeqCrafter/danmu_api</span></div><div class="right"><div class="lazy img" data-bg="https://github.githubassets.com/favicons/favicon.svg"></div></div></a></div><p>上面这个仓库在原作者的基础上添加了直接根据douban_id获取弹幕的功能，因为很多的视频源自带豆瓣ID,这个就避免了错误匹配的可能性。<br>另外，如果视频源不带豆瓣ID或者是一些非国内平台的剧，那么返回弹幕也就没了意义了。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://&#123;your project name&#125;.netlify.app</span><br></pre></td></tr></table></figure><p>那么，在我们网站的设置页面，你的接口应该填写</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://&#123;your project name&#125;.netlify.app/&#123;token&#125;/</span><br></pre></td></tr></table></figure><p>然后就OK了，系统如果检测到你的视频有豆瓣的ID，就会自动抓取弹幕了！</p><p>多说一下，LogVar项目推荐的环境变量：</p><ul><li><code>CONVERT_COLOR</code>: 可以随机为弹幕生成颜色</li><li><code>DANMU_LIMIT</code>: 随机取样限制弹幕数量，弹幕数太多会爆炸</li><li><code>ENABLE_EPISODE_FILTER</code>: 避免抓取到错误链接</li></ul><h2 id="直链播放"><a href="#直链播放" class="headerlink" title="直链播放"></a>直链播放</h2><p>必须提一下直链播放的功能！！<br>你会发现很多采集源的视频质量非常差，模糊不清或者帧率码率很低。所以我添加了这个功能，目的是为了匹配OpenList的资源。<br>OpenList可以解析夸克或者移动这些网盘资源获取直链，那么直接复制来自OpenList的播放链接到里面播放，就可以畅享高清+弹幕的体验了。</p><p>欢迎各位尝试！</p><h2 id="演示站点"><a href="#演示站点" class="headerlink" title="演示站点"></a>演示站点</h2><p>你可以访问</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="NextTV" href="https://libretv.xiaohanys.top/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">NextTV</span><span class="cap link footnote">https://libretv.xiaohanys.top/</span></div><div class="right"><div class="lazy img" data-bg="https://tncache1-f1.v3mh.com/image/2026/01/14/67727e3ade57c7062ef81a16d4f711a0.png"></div></div></a></div><p>来体验我的影视站。</p><h2 id="注意事项及免责声明"><a href="#注意事项及免责声明" class="headerlink" title="注意事项及免责声明"></a>注意事项及免责声明</h2><ul><li>本项目为空壳播放器，自带唯一播放源不稳定，仅供学习使用，请自行更换播放源</li><li>本项目保持轻量(其实复杂的我也不会)，不添加用户登录以及认证功能，不添加成人源隔离</li><li>本项目完全由 Claude Code 生成，仅作为学习参考，请勿用于商业用途</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;AI时代真的给人带来了无限可能，之前不敢想的事情现在都可以做了。个人对 LunaTV 和 LibreTV 都不甚满意，&lt;emp&gt;LunaTV 功能太复杂了，而 LibreTV&lt;/emp&gt;又太简单，页面太赛博朋克了，不是很喜欢。于是借助 AI，我自己实现了我的个人影视站。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站分享" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E5%88%86%E4%BA%AB/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="影视" scheme="https://blog.xiaohanys.top/tags/%E5%BD%B1%E8%A7%86/"/>
    
  </entry>
  
  <entry>
    <title>增加cloudflare Pages的国内访问速度</title>
    <link href="https://blog.xiaohanys.top/accelerate-cf-pages/"/>
    <id>https://blog.xiaohanys.top/accelerate-cf-pages/</id>
    <published>2026-01-06T06:17:48.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>本文通过国内&#x2F;国外 DNS 解析分流的方法，在国外访问速度不变的前提下，大幅提升国内访问的稳定性和速度。</p><p>核心逻辑只有一句话：</p><blockquote><p>国外继续走 Cloudflare, 中国大陆单独走「优选 IP」线路</p></blockquote><p>而实现这个效果，靠的是三点：</p><ul><li>Cloudflare 子域名 NS 下沉</li><li>华为云国际站 线路解析</li><li>第三方 Cloudflare 优选 IP &#x2F; CNAME</li></ul><h3 id="Cloudflare-Pages-正常绑定域名"><a href="#Cloudflare-Pages-正常绑定域名" class="headerlink" title="Cloudflare Pages 正常绑定域名"></a>Cloudflare Pages 正常绑定域名</h3><p>在 Cloudflare Pages 部署后，Cloudflare 会给你分配一个类似这样的 CNAME：<code>xxx.pages.dev</code></p><div class="tag-plugin colorful note" color="blue"><div class="title">注意</div><div class="body"><p>这个CNAME非常重要,后面要用。</p></div></div><p>想要实现优选，这里必须要为pages绑定一个自定义的域名，否则不会生效。这里假设你自己的域名为<code>xiaohanys.top</code>并且已经托管在CF了。<br>再假设你给 Pages 绑定的域名是：<code>blog.xiaohanys.top</code><br>在pages自定义域页面输入域名，点击下一步，点击激活域即可，等待一会后配置完成, 最终效果如图：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:2246/594;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2026/01/06/bb0d958bad813958e5d0f259e9eb69d0.jpg" data-src="https://tncache1-f1.v3mh.com/image/2026/01/06/bb0d958bad813958e5d0f259e9eb69d0.jpg"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><h3 id="注册并使用华为云国际站"><a href="#注册并使用华为云国际站" class="headerlink" title="注册并使用华为云国际站"></a>注册并使用华为云国际站</h3><p>注意是 <strong>国际站</strong>，不是国内站。</p><p>注册地址：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="华为云国际站" href="https://www.huaweicloud.com/intl/zh-cn/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">华为云国际站</span><span class="cap link footnote">https://www.huaweicloud.com/intl/zh-cn/</span></div><div class="right"><div class="lazy img" data-bg="https://www.huaweicloud.com/favicon.ico"></div></div></a></div><p>用邮箱即可注册，无需实名，也无需绑定海外手机。<br>注册完成后进入控制台- 云解析服务 DNS 。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:2906/736;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2026/01/06/1d9309bef11fdaa3a07c28b05326c2fb.jpg" data-src="https://tncache1-f1.v3mh.com/image/2026/01/06/1d9309bef11fdaa3a07c28b05326c2fb.jpg"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><h3 id="在华为云添加子域名解析"><a href="#在华为云添加子域名解析" class="headerlink" title="在华为云添加子域名解析"></a>在华为云添加子域名解析</h3><div class="tag-plugin timeline"><div class="timenode" index="0"><div class="header"><span>添加域名</span></div><div class="body fs14"><p>在华为云 DNS 中，创建公网域名：<code>blog.xiaohanys.top</code><br>⚠️ 这里只添加二级域名，不是主域名。因为我的主域名托管在cloudflare</p></div></div><div class="timenode" index="1"><div class="header"><span>添加一条 DNS 记录</span></div><div class="body fs14"><p>第一条：默认线路（非大陆）</p><ul><li>类型：CNAME</li><li>主机记录：空</li><li>线路类型：全网默认</li><li>解析值：Cloudflare Pages 分配的 xxx.pages.dev</li></ul><p><img src="https://tncache1-f1.v3mh.com/image/2026/01/06/965ab63e249cc50c71679515a6cdbe96.md.jpg"></p><p>📌 这条保证海外访问</p><ul><li>非大陆线路</li><li>正常走 Cloudflare 官方网络。</li></ul></div></div><div class="timenode" index="2"><div class="header"><span>添加第二条 DNS 记录</span></div><div class="body fs14"><p>第二条：中国大陆线路（优选 IP）</p><ul><li>类型：CNAME</li><li>主机记录：空</li><li>线路类型：地域解析&#x2F;中国大陆</li><li>解析值：第三方提供的 优选 IP CNAME<br>例如：<code>youxuan.cf.090227.xyz</code></li></ul><p><img src="https://tncache1-f1.v3mh.com/image/2026/01/06/af95247e7b466e3de81867c52b72f6a4.md.jpg"></p><p>📌 这条才是核心：大陆访问全部走优选 IP</p></div></div><div class="timenode" index="3"><div class="header"><span>将NS填入cloudflare的DNS解析</span></div><div class="body fs14"><p>记录华为云分配的 NS 服务器<br>域名添加完成后，<br>华为云会给这个子域名分配一组 NS，例如：</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">ns1.huaweicloud-dns.com</span><br><span class="line">ns1.huaweicloud-dns.cn</span><br><span class="line">ns1.huaweicloud-dns.net</span><br><span class="line">ns1.huaweicloud-dns.org</span><br></pre></td></tr></table></figure><p>将这组NS填入Cloudflare的NS解析记录如图所示：</p><p><img src="https://tncache1-f1.v3mh.com/image/2026/01/06/a684dcf19249b00d3a2749e20ac304df.jpg"><br>📌 这一步的含义是：<br><code>blog.xiaohanys.top</code> 这个子域名DNS 解析权下放给华为云,而主域名仍然完全由 Cloudflare 管理。</p></div></div></div><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>配置完成后，一般几分钟到十几分钟生效。</p><p>你可以通过以下方式验证：</p><ul><li>itdog等网页tcping测试域名</li><li>浏览器直接访问 Logo</li><li>播放器里加载台标</li><li>使用不同网络测试（大陆 &#x2F; 非大陆）</li></ul><p>通过 Cloudflare Pages + 华为云国际站 + 优选 IP，<br>你可以在 不更换平台、不改项目 的前提下：</p><ul><li>🚀 明显改善国内访问速度</li><li>🌍 保留 Cloudflare 全球 CDN</li><li>🔐 架构清晰、风险可控</li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;本文通过国内&amp;#x2F;国外 DNS 解析分流的方法，在国外访问速度不变的前提下，大幅提升国内访问的稳定性和速度。&lt;/p&gt;
&lt;p&gt;核心逻辑只有一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;国外继续走 Cloudflare, 中国大陆单独走「优选</summary>
        
      
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
  </entry>
  
  <entry>
    <title>为影视站添加弹幕功能</title>
    <link href="https://blog.xiaohanys.top/video-barrage/"/>
    <id>https://blog.xiaohanys.top/video-barrage/</id>
    <published>2026-01-03T05:06:16.000Z</published>
    <updated>2026-03-26T09:21:36.505Z</updated>
    
    <content type="html"><![CDATA[<p>大家从采集站看爱优腾的视频的时候，是否为无法一同看到弹幕而忧愁，其实我们可以通过爬虫从爱优腾抓取弹幕内容，添加到播放器中，从而实现沉浸式弹幕观看。</p><span id="more"></span><div class="tag-plugin blockquote" indent="undefined"><p>原理就是多数的采集网站通常会自带豆瓣的影视 ID，那么我们就可以通过这个 ID 确定视频的官方来源，通过豆瓣抓取对应的影视官方的视频链接，然后再通过该链接抓取到对应的弹幕。</p></div><p>我们这里有两个例子，都是魔改的其他开源项目：</p><h2 id="基于-python-的弹幕抓取项目"><a href="#基于-python-的弹幕抓取项目" class="headerlink" title="基于 python 的弹幕抓取项目"></a>基于 python 的弹幕抓取项目</h2><h3 id="项目仓库"><a href="#项目仓库" class="headerlink" title="项目仓库"></a>项目仓库</h3><div class="tag-plugin link dis-select"><a class="link-card plain" title="SeqCrafter/fetch_danmu" href="https://github.com/SeqCrafter/fetch_danmu" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">SeqCrafter/fetch_danmu</span><span class="cap link footnote">https://github.com/SeqCrafter/fetch_danmu</span></div><div class="right"><div class="lazy img" data-bg="https://github.githubassets.com/favicons/favicon.svg"></div></div></a></div><p>这个爬虫方案强烈建议直接部署在本地，或者国内的服务器上。因为我测试部署在国外的平台都会导致抓取不到链接，或者抓取到错误的链接的情况。</p><h3 id="部署方式"><a href="#部署方式" class="headerlink" title="部署方式"></a>部署方式</h3><p>本项目采用 docker 部署：</p><figure class="highlight bash"><table><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">docker run -d \</span><br><span class="line">   --name fetch_danmu \</span><br><span class="line">   --restart unless-stopped \</span><br><span class="line">   -p 8080:8080 \</span><br><span class="line">   ghcr.io/seqcrafter/fetch_danmu:latest</span><br></pre></td></tr></table></figure><h3 id="API-接口"><a href="#API-接口" class="headerlink" title="API 接口"></a>API 接口</h3><p>假设部署在本地，接口地址为：<code>http://127.0.0.1:8080/douban_id</code>，参数为：</p><ul><li><code>douban_id</code> : 豆瓣的 ID</li><li><code>episode_number</code> : 剧集集数</li></ul><p>本地 curl 的调用示例：</p><figure class="highlight bash"><table><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"><span class="comment">## 子夜归第一集弹幕</span></span><br><span class="line">curl <span class="string">&quot;http://127.0.0.1:8080/douban_id?douban_id=36481469&amp;episode_number=1&quot;</span></span><br></pre></td></tr></table></figure><p>返回格式中的 danmuku 数据可以直接传给播放器例如<a href="https://github.com/Hiram-Wong/ZyPlayer">ZyPlayer</a>或者<a href="https://github.com/SeqCrafter/LibreTV">LibreTV</a>等使用。</p><figure class="highlight json"><table><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;code&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;36172040&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;danmu&quot;</span><span class="punctuation">:</span> <span class="number">13223</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;danmuku&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">[</span><span class="number">0.0</span><span class="punctuation">,</span> <span class="string">&quot;right&quot;</span><span class="punctuation">,</span> <span class="string">&quot;#FFFFFF&quot;</span><span class="punctuation">,</span> <span class="string">&quot;25px&quot;</span><span class="punctuation">,</span> <span class="string">&quot;恭迎师祖出山&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">[</span><span class="number">0.0</span><span class="punctuation">,</span> <span class="string">&quot;right&quot;</span><span class="punctuation">,</span> <span class="string">&quot;#FFFFFF&quot;</span><span class="punctuation">,</span> <span class="string">&quot;25px&quot;</span><span class="punctuation">,</span> <span class="string">&quot;来支持献鱼啦&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="基于-nodejs-的弹幕抓取项目"><a href="#基于-nodejs-的弹幕抓取项目" class="headerlink" title="基于 nodejs 的弹幕抓取项目"></a>基于 nodejs 的弹幕抓取项目</h2><h3 id="项目仓库-1"><a href="#项目仓库-1" class="headerlink" title="项目仓库"></a>项目仓库</h3><div class="tag-plugin link dis-select"><a class="link-card plain" title="SeqCrafter/danmu_api" href="https://github.com/SeqCrafter/danmu_api" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">SeqCrafter/danmu_api</span><span class="cap link footnote">https://github.com/SeqCrafter/danmu_api</span></div><div class="right"><div class="lazy img" data-bg="https://github.githubassets.com/favicons/favicon.svg"></div></div></a></div><p>这个可以部署到 vercel 或者 netlify, 部署到远端更方便</p><h3 id="部署方式-1"><a href="#部署方式-1" class="headerlink" title="部署方式"></a>部署方式</h3><p>例如部署到 netlify:</p><p>直接 fork 本仓库，然后在 netlify 中创建项目，一键开启即可.</p><h3 id="API-接口-1"><a href="#API-接口-1" class="headerlink" title="API 接口"></a>API 接口</h3><p>假设部署在本地，接口地址为：<code>https://&#123;你的部署项目名&#125;.netlify.app/api/v2/douban</code>，参数为：</p><ul><li><code>douban_id</code> : 豆瓣的 ID</li><li><code>episode_number</code> : 剧集集数</li></ul><p>本地 curl 的调用示例：</p><figure class="highlight bash"><table><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"><span class="comment">## 子夜归第一集弹幕</span></span><br><span class="line">curl <span class="string">&quot;https://&#123;你的部署项目名&#125;.netlify.app/api/v2/douban?douban_id=36481469&amp;episode_number=1&quot;</span></span><br></pre></td></tr></table></figure><p>返回格式中的 danmuku 数据可以直接传给播放器例如<a href="https://github.com/Hiram-Wong/ZyPlayer">ZyPlayer</a>或者<a href="https://github.com/SeqCrafter/LibreTV">LibreTV</a>等使用。</p><figure class="highlight json"><table><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;code&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;success&quot;</span><span class="punctuation">:</span> <span class="string">&quot;true&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;errorMessage&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;danmu&quot;</span><span class="punctuation">:</span> <span class="number">1230</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;danmuku&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">[</span><span class="number">0.0</span><span class="punctuation">,</span> <span class="string">&quot;right&quot;</span><span class="punctuation">,</span> <span class="string">&quot;#FFFFFF&quot;</span><span class="punctuation">,</span> <span class="string">&quot;25px&quot;</span><span class="punctuation">,</span> <span class="string">&quot;恭迎师祖出山&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">[</span><span class="number">0.0</span><span class="punctuation">,</span> <span class="string">&quot;right&quot;</span><span class="punctuation">,</span> <span class="string">&quot;#FFFFFF&quot;</span><span class="punctuation">,</span> <span class="string">&quot;25px&quot;</span><span class="punctuation">,</span> <span class="string">&quot;来支持献鱼啦&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="想试试？"><a href="#想试试？" class="headerlink" title="想试试？"></a>想试试？</h2><p>打开我部署的 LibreTV 项目：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="LibreTV" href="https://libretv.xiaohanys.top/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">LibreTV</span><span class="cap link footnote">https://libretv.xiaohanys.top/</span></div><div class="right"><div class="lazy img" data-bg="https://libretv.xiaohanys.top/image/logo.png"></div></div></a></div><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:3016/1396;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2026/01/03/43902ff7765a19c3c920e3fa3edea59f.jpg" data-src="https://tncache1-f1.v3mh.com/image/2026/01/03/43902ff7765a19c3c920e3fa3edea59f.jpg"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><p>按照图上设置，首先打开资源开关，然后在弹幕设置中填入你部署好的 API 地址，名称随便写，地址写<code>https://&#123;你的部署项目名&#125;.netlify.app/api/v2/douban</code></p><p>随便打开一个视频测试一下吧！</p><div class="tag-plugin colorful note" color="orange"><div class="title">警告</div><div class="body"><p>我部署的libreTV的资源调用是有限的，请不要把这个站点当做你的观看站点，如有需要，请自行部署</p></div></div><h2 id="一个自带弹幕的视频播放应用-lunaTV"><a href="#一个自带弹幕的视频播放应用-lunaTV" class="headerlink" title="一个自带弹幕的视频播放应用-lunaTV"></a>一个自带弹幕的视频播放应用-lunaTV</h2><div class="tag-plugin link dis-select"><a class="link-card plain" title="LunaTV" href="https://github.com/SzeMeng76/LunaTV" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">LunaTV</span><span class="cap link footnote">https://github.com/SzeMeng76/LunaTV</span></div><div class="right"><div class="lazy img" data-bg="https://github.githubassets.com/favicons/favicon.svg"></div></div></a></div><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:2942/1438;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2026/01/05/e1f0aa696cbe595464cb29dfc84dfc8c.png" data-src="https://tncache1-f1.v3mh.com/image/2026/01/05/e1f0aa696cbe595464cb29dfc84dfc8c.png"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><h3 id="部署方法-zeabur-upstash"><a href="#部署方法-zeabur-upstash" class="headerlink" title="部署方法(zeabur+upstash)"></a>部署方法(zeabur+upstash)</h3><p><a href="https://zeabur.com/zh-CN/">zeabur</a> 是一站式云端部署平台，使用预构建的 Docker 镜像可以快速部署，无需等待构建。</p><p>部署步骤：</p><div class="tag-plugin timeline"><div class="timenode" index="0"><div class="header"><span>添加 LunaTV 服务</span></div><div class="body fs14"><ul><li>点击 “Add Service” &gt; “Docker Images”</li><li>输入镜像名称：ghcr.io&#x2F;szemeng76&#x2F;lunatv:latest</li><li>配置端口：3000 (HTTP)</li></ul></div></div><div class="timenode" index="1"><div class="header"><span>添加 upstash 服务</span></div><div class="body fs14"><ul><li>访问 <a href="https://upstash.com/">upstash.com</a></li><li>注册账号并创建新的 Redis 数据库</li><li>选择区域（建议选择离你最近的区域）</li><li>复制 REST URL 和 REST TOKEN</li></ul></div></div><div class="timenode" index="2"><div class="header"><span>配置环境变量</span></div><div class="body fs14"><p>在 LunaTV 服务的环境变量中添加：</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 必填：管理员账号</span></span><br><span class="line">USERNAME=admin</span><br><span class="line">PASSWORD=your_secure_password</span><br><span class="line"></span><br><span class="line"><span class="comment"># 必填：存储配置</span></span><br><span class="line">NEXT_PUBLIC_STORAGE_TYPE=upstash</span><br><span class="line">UPSTASH_URL=https://your-redis-instance.upstash.io</span><br><span class="line">UPSTASH_TOKEN=AxxxxxxxxxxxxxxxxxxxxxxxxxxxQ==</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可选：站点配置</span></span><br><span class="line">SITE_BASE=https://your-domain.zeabur.app</span><br><span class="line">NEXT_PUBLIC_SITE_NAME=LunaTV Enhanced</span><br><span class="line">ANNOUNCEMENT=欢迎使用 LunaTV Enhanced Edition</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可选：豆瓣代理配置（推荐）</span></span><br><span class="line">NEXT_PUBLIC_DOUBAN_PROXY_TYPE=cmliussss-cdn-tencent</span><br><span class="line">NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE=cmliussss-cdn-tencent</span><br></pre></td></tr></table></figure></div></div><div class="timenode" index="3"><div class="header"><span>设置访问域名（必须）</span></div><div class="body fs14"><ul><li>在 LunaTV 服务页面，点击 “Networking” 或 “网络” 标签</li><li>点击 “Generate Domain” 生成 Zeabur 提供的免费域名（如 xxx.zeabur.app）</li><li>或者绑定自定义域名：<ul><li>点击 “Add Domain” 添加你的域名</li><li>按照提示配置 DNS CNAME 记录指向 Zeabur 提供的目标地址</li></ul></li><li>设置完域名后即可通过域名访问 LunaTV</li></ul></div></div></div><div class="tag-plugin colorful note" color="orange"><div class="title">注意</div><div class="body"><p>部署好的网站为空壳,需要你进入管理员设置自行添加视频源！</p></div></div><p>演示站：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="smonetv" href="https://smonetv.zeabur.app/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="desc"><div class="left"><span class="title">smonetv</span><span class="cap link footnote">https://smonetv.zeabur.app/</span></div><div class="right"><div class="lazy img" data-bg="https://smonetv.zeabur.app/icons/icon-192x192.png"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;大家从采集站看爱优腾的视频的时候，是否为无法一同看到弹幕而忧愁，其实我们可以通过爬虫从爱优腾抓取弹幕内容，添加到播放器中，从而实现沉浸式弹幕观看。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="娱乐" scheme="https://blog.xiaohanys.top/tags/%E5%A8%B1%E4%B9%90/"/>
    
    <category term="影视" scheme="https://blog.xiaohanys.top/tags/%E5%BD%B1%E8%A7%86/"/>
    
    <category term="爬虫" scheme="https://blog.xiaohanys.top/tags/%E7%88%AC%E8%99%AB/"/>
    
  </entry>
  
  <entry>
    <title>如何使用Umami来添加网站统计</title>
    <link href="https://blog.xiaohanys.top/umami-state/"/>
    <id>https://blog.xiaohanys.top/umami-state/</id>
    <published>2025-12-31T03:04:53.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>虽然我们可以使用 busuanzi 简单的添加网站统计，但是由于已经添加了 Umami 统计，那么就没必要再额外的使用 busuanzi 做一层计算了，而且 Umami 的统计更全面可靠，那么我们如何获取 umami 的统计信息并添加到网站呢？</p><span id="more"></span><p>有两种方法。</p><ul><li>第一种是调用官网 API 的方法，这一种为了避免 Token 泄露，需要添加一层中间代理，比如使用 CloudFlare Worker 或者 EdgeOne page。</li><li>第二种是采用爬虫策略直接爬取 Share 链接的信息。</li></ul><h2 id="调用官网-API-的方法"><a href="#调用官网-API-的方法" class="headerlink" title="调用官网 API 的方法"></a>调用官网 API 的方法</h2><blockquote><p>需要注意，这种方法存在限制，官方 API 有速率限制，而中间层的边缘计算具有次数限制，小量使用完全没问题，但是大量的话会出问题。</p></blockquote><h3 id="云函数代理"><a href="#云函数代理" class="headerlink" title="云函数代理"></a>云函数代理</h3><p>我这里使用腾讯 EdgeOne 的<code>Page Function</code>作为中间层代理，创建目录<code>./edge-functions/umami.js</code>并写入：</p><figure class="highlight javascript"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">onRequest</span>(<span class="params">context</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">UMAMI_BASE</span> = <span class="string">&quot;https://api.umami.is/v1&quot;</span>; <span class="comment">// 或你的自建 Umami</span></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">WEBSITE_ID</span> = context.<span class="property">env</span>.<span class="property">UMAMI_WEBSITE_ID</span>;</span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">TOKEN</span> = context.<span class="property">env</span>.<span class="property">UMAMI_API_TOKEN</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> createdAt = <span class="string">&quot;2025-12-30T17:21:06.019Z&quot;</span>;</span><br><span class="line">  <span class="keyword">const</span> start = <span class="keyword">new</span> <span class="title class_">Date</span>(createdAt).<span class="title function_">getTime</span>();</span><br><span class="line">  <span class="keyword">const</span> end = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> apiUrl = <span class="string">`<span class="subst">$&#123;UMAMI_BASE&#125;</span>/websites/<span class="subst">$&#123;WEBSITE_ID&#125;</span>/stats?startAt=<span class="subst">$&#123;start&#125;</span>&amp;endAt=<span class="subst">$&#123;end&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(apiUrl, &#123;</span><br><span class="line">      <span class="attr">headers</span>: &#123;</span><br><span class="line">        <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">$&#123;TOKEN&#125;</span>`</span>,</span><br><span class="line">        <span class="title class_">Accept</span>: <span class="string">&quot;application/json&quot;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!res.<span class="property">ok</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; <span class="attr">error</span>: <span class="string">&quot;Umami API error&quot;</span> &#125;), &#123;</span><br><span class="line">        <span class="attr">status</span>: res.<span class="property">status</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> data = <span class="keyword">await</span> res.<span class="title function_">json</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(</span><br><span class="line">      <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123;</span><br><span class="line">        <span class="attr">pageviews</span>: data.<span class="property">pageviews</span>,</span><br><span class="line">        <span class="attr">visitors</span>: data.<span class="property">visitors</span>,</span><br><span class="line">        <span class="attr">visits</span>: data.<span class="property">visits</span>,</span><br><span class="line">        <span class="attr">bounces</span>: data.<span class="property">bounces</span>,</span><br><span class="line">        <span class="attr">totaltime</span>: data.<span class="property">totaltime</span>,</span><br><span class="line">        <span class="attr">updatedAt</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line">      &#125;),</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">headers</span>: &#123;</span><br><span class="line">          <span class="string">&quot;Access-Control-Allow-Origin&quot;</span>: <span class="string">&quot;*&quot;</span>,</span><br><span class="line">          <span class="string">&quot;Access-Control-Allow-Methods&quot;</span>:</span><br><span class="line">            <span class="string">&quot;GET, POST, PUT, DELETE, PATCH, OPTIONS&quot;</span>,</span><br><span class="line">          <span class="string">&quot;Access-Control-Allow-Headers&quot;</span>: <span class="string">&quot;Content-Type, Authorization&quot;</span>,</span><br><span class="line">          <span class="string">&quot;Access-Control-Max-Age&quot;</span>: <span class="string">&quot;86400&quot;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;</span><br><span class="line">    );</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(</span><br><span class="line">      <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; <span class="attr">error</span>: e?.<span class="property">message</span> || <span class="string">&quot;Internal error&quot;</span> &#125;),</span><br><span class="line">      &#123; <span class="attr">status</span>: <span class="number">500</span> &#125;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>简单解释一下，获取统计信息主要是访问<code>https://api.umami.is/v1/websites/$&#123;WEBSITE_ID&#125;/stats</code>获得，其中 <code>startAt</code> 和 <code>endAt</code>是必须参数，他们是以毫秒为单位的时间戳，<code>createdAt</code>修改为你的网站接入统计的日期，然后<code>endAt</code>使用当前时间戳既可以获取所有时间段的访问量统计，返回值中的<code>pageviews</code> 即为浏览量，而<code>visitors</code> 即为非冗余游客量。当然，访问的时候需要带上<code>Authorization token</code>。</p><p><strong>如何获取<code>Authorization token</code> ？</strong></p><p>访问这个<a href="https://cloud.umami.is/settings/api-keys">链接</a> 获取即可，注意保密！</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:2486/1102;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2025/12/31/533f3073cfa1ce3ceb59b8aa78f4a83c.jpg" data-src="https://tncache1-f1.v3mh.com/image/2025/12/31/533f3073cfa1ce3ceb59b8aa78f4a83c.jpg" alt="获取API-KEY"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">获取API-KEY</span></div></div><p><strong>如何获取网站 ID</strong></p><p>鼠标点击你的网站名字，新打开的 url 最后面结尾那一串字符串就是你的网站 ID.</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:2408/792;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2025/12/31/d37902dca5cc3e8e10e27606380b6c64.jpg" data-src="https://tncache1-f1.v3mh.com/image/2025/12/31/d37902dca5cc3e8e10e27606380b6c64.jpg" alt="获取网站id"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">获取网站id</span></div></div><p>edgeOne 的演示地址：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="edgeonePanel" href="https://landing-page-demo-77qf8sw1.edgeone.app" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="icon,desc"><div class="left"><span class="title">edgeonePanel</span><span class="cap link footnote">https://landing-page-demo-77qf8sw1.edgeone.app</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>然后你就可以在你的页脚处添加一个 html 如下：</p><figure class="highlight html"><table><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"><span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">&quot;umami_container_site_pv&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span>本站总访问量<span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">&quot;umami_value_site_pv&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span>次, 总访客量<span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">    <span class="attr">id</span>=<span class="string">&quot;umami_value_site_v&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span>&lt;/span</span><br><span class="line">  &gt;人&lt;/span</span><br><span class="line">&gt;</span><br></pre></td></tr></table></figure><p>然后写一个 javascript 脚本来填充数据.</p><figure class="highlight javascript"><table><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">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">API_URL</span> = <span class="string">&quot;https://landing-page-demo-77qf8sw1.edgeone.app/umami&quot;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> pvEl = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;umami_value_site_pv&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> vEl = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;umami_value_site_v&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!pvEl || !vEl) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">fetch</span>(<span class="variable constant_">API_URL</span>, &#123; <span class="attr">cache</span>: <span class="string">&quot;no-store&quot;</span> &#125;)</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">(<span class="params">res</span>) =&gt;</span> res.<span class="title function_">json</span>())</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> data.<span class="property">pageviews</span> === <span class="string">&quot;number&quot;</span>) &#123;</span><br><span class="line">        pvEl.<span class="property">textContent</span> = data.<span class="property">pageviews</span>.<span class="title function_">toLocaleString</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> data.<span class="property">visitors</span> === <span class="string">&quot;number&quot;</span>) &#123;</span><br><span class="line">        vEl.<span class="property">textContent</span> = data.<span class="property">visitors</span>.<span class="title function_">toLocaleString</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">catch</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      pvEl.<span class="property">textContent</span> = <span class="string">&quot;-&quot;</span>;</span><br><span class="line">      vEl.<span class="property">textContent</span> = <span class="string">&quot;-&quot;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><h3 id="爬虫抓取"><a href="#爬虫抓取" class="headerlink" title="爬虫抓取"></a>爬虫抓取</h3><blockquote><p>该方案来自博客二叉树树</p></blockquote><p>首先我们启用分享 URL</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1302/686;"><img class="lazy" src="https://blog.acofork.com/_astro/023f687b-6e4a-46d8-b7f2-4778f20ebe99.BNFPkmbp.webp" data-src="https://blog.acofork.com/_astro/023f687b-6e4a-46d8-b7f2-4778f20ebe99.BNFPkmbp.webp" alt="开启分享"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">开启分享</span></div></div><p>注意这里的 <code>7PoDRgCzHFTs2vWB</code> ，每个站点都不一样</p><p>接着我们请求 <code>https://cloud.umami.is/analytics/us/api/share/7PoDRgCzHFTs2vWB</code>，得到 注意，这里的 <code>us</code> 为你创建的账号区域，美国为<code>us</code>，欧盟为<code>eu</code>.</p><figure class="highlight json"><table><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;websiteId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;a66a5fd4-98b0-4108-8606-cb7094f380ac&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3ZWJzaXRlSWQiOiJhNjZhNWZkNC05OGIwLTQxMDgtODYwNi1jYjcwOTRmMzgwYWMiLCJpYXQiOjE3NTA4MDIwMzB9.X5GQT5kslh6r25sFlap4Asz1NDA7mN3kcZW8wqbrnBc&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>再接着我们请求，携带请求头 <code>x-umami-share-token</code> 值为上一步获得的<code>Token</code></p><p><code>https://cloud.umami.is/analytics/us/api/websites/a66a5fd4-98b0-4108-8606-cb7094f380ac/stats?startAt=0&amp;endAt=1750805999999&amp;unit=hour&amp;timezone=Asia/Hong_Kong&amp;path=eq./posts/cf-fastip/&amp;compare=false</code></p><p>这里解释几个关键<code>Params</code>，其他的照搬</p><ul><li><p><code>startAt</code>：统计开始时间。Unix 时间戳，我们填写为 0 让 Umami 从 1970 年开始统计</p></li><li><p><code>endAt</code>：统计结束时间。Unix 时间戳，我们可以使用 <code>Date.now()</code> ，即当前时间，和 startAt 参数联动即可实现统计总浏览量</p></li><li><p><code>path</code>：要查询的路径，填写为你的文章页去除了 Host 的路径，如 <code>/posts/hello</code> 。注意！Umami 会将 <code>/posts/hello</code> 和 <code>/posts/hello/</code> 视为两个不同的路径，请注意你的博客框架是否使用<code> /</code>。需要使用 <code>eq.</code> 前缀来进行精确匹配，例如 <code>path=eq./posts/hello/</code></p></li></ul><p>你会得到</p><figure class="highlight json"><table><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;pageviews&quot;</span><span class="punctuation">:</span> <span class="number">1655</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;visitors&quot;</span><span class="punctuation">:</span> <span class="number">343</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;visits&quot;</span><span class="punctuation">:</span> <span class="number">411</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;bounces&quot;</span><span class="punctuation">:</span> <span class="number">183</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;totaltime&quot;</span><span class="punctuation">:</span> <span class="number">30592</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;comparison&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;pageviews&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;visitors&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;visits&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;bounces&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;totaltime&quot;</span><span class="punctuation">:</span> <span class="number">0</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>前面已经讲过了，<code>pageviews</code> 即浏览量。 <code>visitors</code> 即访问人数。</p><p>那么，重复前面的步骤，我们就可以修改前端的 javascript 脚本来直接获取该逆向的结果：</p><p>依旧创建 html</p><figure class="highlight html"><table><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"><span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">&quot;umami_container_site_pv&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span>本站总访问量<span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">&quot;umami_value_site_pv&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span>次, 总访客量<span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">    <span class="attr">id</span>=<span class="string">&quot;umami_value_site_v&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span>&lt;/span</span><br><span class="line">  &gt;人&lt;/span</span><br><span class="line">&gt;</span><br></pre></td></tr></table></figure><p>然后写一个 javascript 脚本来填充数据.</p><figure class="highlight javascript"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">(<span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> websiteID = <span class="string">&quot;a66a5fd4-98b0-4108-8606-cb7094f380ac&quot;</span>;</span><br><span class="line">  <span class="keyword">const</span> startAt = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">const</span> endAt = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">API_URL</span> = <span class="string">`https://cloud.umami.is/analytics/us/api/websites/<span class="subst">$&#123;websiteID&#125;</span>/stats?startAt=<span class="subst">$&#123;startAt&#125;</span>&amp;endAt=<span class="subst">$&#123;endAt&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> pvEl = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;umami_value_site_pv&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> vEl = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;umami_value_site_v&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!pvEl || !vEl) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">fetch</span>(<span class="variable constant_">API_URL</span>, &#123;</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="string">&quot;x-umami-share-token&quot;</span>:</span><br><span class="line">        <span class="string">&quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3ZWJzaXRlSWQiOiJhNjZhNWZkNC05OGIwLTQxMDgtODYwNi1jYjcwOTRmMzgwYWMiLCJpYXQiOjE3NTA4MDIwMzB9.X5GQT5kslh6r25sFlap4Asz1NDA7mN3kcZW8wqbrnBc&quot;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;)</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">(<span class="params">res</span>) =&gt;</span> res.<span class="title function_">json</span>())</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> data.<span class="property">pageviews</span> === <span class="string">&quot;number&quot;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">pageviews</span>);</span><br><span class="line">        pvEl.<span class="property">textContent</span> = data.<span class="property">pageviews</span>.<span class="title function_">toLocaleString</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> data.<span class="property">visitors</span> === <span class="string">&quot;number&quot;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(data.<span class="property">visitors</span>);</span><br><span class="line">        vEl.<span class="property">textContent</span> = data.<span class="property">visitors</span>.<span class="title function_">toLocaleString</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">catch</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      pvEl.<span class="property">textContent</span> = <span class="string">&quot;-&quot;</span>;</span><br><span class="line">      vEl.<span class="property">textContent</span> = <span class="string">&quot;-&quot;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>因为我想获取全站的总体统计信息，因此只需要两个请求参数<code>startAt</code>和<code>endAt</code>即可。</p><p>完！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;虽然我们可以使用 busuanzi 简单的添加网站统计，但是由于已经添加了 Umami 统计，那么就没必要再额外的使用 busuanzi 做一层计算了，而且 Umami 的统计更全面可靠，那么我们如何获取 umami 的统计信息并添加到网站呢？&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>我的hexo-theme-stellar装修记录</title>
    <link href="https://blog.xiaohanys.top/hexo-theme-stellar/"/>
    <id>https://blog.xiaohanys.top/hexo-theme-stellar/</id>
    <published>2025-12-29T13:20:12.000Z</published>
    <updated>2026-03-26T09:21:36.505Z</updated>
    
    <content type="html"><![CDATA[<p>搭建过很多的个人博客，从 WordPress, Typecho 到 Halo，甚至是 Nuxt 自建的博客，但最终感觉都不方便，还是 Hexo 博客写起来更方便，托管也无需服务器。一开始尝试了 <a href="https://volantis.js.org/v6/getting-started/">Hexo-theme-volantis</a>, 但是总感觉不够完美，配置起来也是相当的复杂和麻烦，于是切换到了<a href="https://xaoxuu.com/wiki/stellar/">Hexo-theme-Stellar</a>, 今天总结一下在切换到主题后需要配置和修改的地方。<u>需要注意的是，我不打算修改主题文件，一个原因是原主题足够优秀，还有就是怕麻烦。所以这里的修改其实只涉及配置文件的更改。</u></p><h3 id="如何切换主题"><a href="#如何切换主题" class="headerlink" title="如何切换主题"></a>如何切换主题</h3><p>首先安装主题：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i hexo-theme-stellar</span><br></pre></td></tr></table></figure><p>然后在 hexo 的配置文件<code>_config.yml</code>文件中找到并修改：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">theme:</span> <span class="string">stellar</span></span><br></pre></td></tr></table></figure><h3 id="网站信息的修改"><a href="#网站信息的修改" class="headerlink" title="网站信息的修改"></a>网站信息的修改</h3><p>这个主要涉及头像，标题和副标题。副标题支持鼠标移动到上方切换文字，所以你可以使用竖线分割两份文字。在主题配置文件<code>_config.stellar.yml</code>添加以下内容：</p><figure class="highlight yaml"><table><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></pre></td><td class="code"><pre><span class="line"><span class="attr">logo:</span></span><br><span class="line">  <span class="attr">title:</span> <span class="string">&quot;[XiaoHanYS](/)&quot;</span></span><br><span class="line">  <span class="attr">avatar:</span> <span class="string">/images/avatar.jpg</span></span><br><span class="line">  <span class="attr">subtitle:</span> <span class="string">&quot;期待光明 | www.xiaohanys.top&quot;</span></span><br></pre></td></tr></table></figure><h3 id="首页四个导航按钮"><a href="#首页四个导航按钮" class="headerlink" title="首页四个导航按钮"></a>首页四个导航按钮</h3><p>首先按钮的<code>Icon</code>来自于<a href="https://github.com/xaoxuu/hexo-theme-stellar/blob/main/_data/icons.yml">icons.yml</a>, 因此你可以根据需要挑选合适的作为首页的按钮图标。我这里让 AI 帮我生成了两个新的，自定义的图标我们应该放在<code>source/_data/icons.yml</code>中。</p><p>剩下的就是为每个导航添加<code>ID</code>和导航到的<code>url</code>路径，因为我没有添加文档功能，所以导航到<code>404页面</code>.</p><p>在主题配置文件<code>_config.stellar.yml</code>添加以下内容：</p><figure class="highlight yaml"><table><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="attr">menubar:</span></span><br><span class="line">  <span class="attr">columns:</span> <span class="number">4</span> <span class="comment"># 一行多少个</span></span><br><span class="line">  <span class="attr">items:</span> <span class="comment"># 可按照自己需求增加，符合以下格式即可</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">post</span> <span class="comment"># 页面中高亮的 menu_id</span></span><br><span class="line">      <span class="attr">theme:</span> <span class="string">&quot;#1BCDFC&quot;</span> <span class="comment"># 高亮时的颜色，仅 svg 中 fill=&quot;currentColor&quot; 时有效</span></span><br><span class="line">      <span class="attr">icon:</span> <span class="string">solar:documents-bold-duotone</span> <span class="comment"># 支持 svg/img 标签，可以定义在 icons.yml 文件中，也支持外部图片的 URL</span></span><br><span class="line">      <span class="attr">title:</span> <span class="string">博客</span> <span class="comment"># 标题</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">/</span> <span class="comment"># 跳转链接，支持相对路径和绝对路径</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">wiki</span></span><br><span class="line">      <span class="attr">theme:</span> <span class="string">&quot;#3DC550&quot;</span></span><br><span class="line">      <span class="attr">icon:</span> <span class="string">solar:notebook-bookmark-bold-duotone</span></span><br><span class="line">      <span class="attr">title:</span> <span class="string">文档</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">/404.html</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">gallery</span></span><br><span class="line">      <span class="attr">theme:</span> <span class="string">&quot;#FA6400&quot;</span></span><br><span class="line">      <span class="attr">icon:</span> <span class="string">solar:image_gallary_duotone</span></span><br><span class="line">      <span class="attr">title:</span> <span class="string">图库</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">/gallery/</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">project</span></span><br><span class="line">      <span class="attr">theme:</span> <span class="string">&quot;#F44336&quot;</span></span><br><span class="line">      <span class="attr">icon:</span> <span class="string">solar:project_duotone</span></span><br><span class="line">      <span class="attr">title:</span> <span class="string">项目</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">/projects/</span></span><br></pre></td></tr></table></figure><h3 id="添加自定义界面"><a href="#添加自定义界面" class="headerlink" title="添加自定义界面"></a>添加自定义界面</h3><p>从上面的导航按钮，你可能发现我添加了图库和项目进展两个独立页面。添加独立页面的方式是在<code>source</code> 目录内新建一个目录，目录的名称为新页面的 url, 而后在目录内添加<code>index.md</code>文件即可，例如<code>/gallery</code>对应<code>source/gallery/index.md</code>.</p><p>图库的语法为, 你可以添加很多的图片，会自动排版！</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">&#123;% gallery %&#125;</span><br><span class="line">![图片名称](图片链接)</span><br><span class="line">...</span><br><span class="line">&#123;% endgallery %&#125;</span><br></pre></td></tr></table></figure><p><a href="/gallery/">点我查看示例</a></p><p>项目目录使用了<code>时间线</code>功能，而 Github 仓库使用<code>ghcard</code>功能，语法分别为：</p><figure class="highlight markdown"><table><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">&#123;% timeline %&#125;</span><br><span class="line"></span><br><span class="line">&lt;!-- node 2025 年 10 月 1 日 --&gt;</span><br><span class="line"></span><br><span class="line">修改 libreTV 项目，添加支持弹幕的功能，可以搭配 fetch<span class="emphasis">_danmu 使用，支持 EdgeOne 部署。</span></span><br><span class="line"><span class="emphasis">Github 仓库为：</span></span><br><span class="line"><span class="emphasis">&#123;% ghcard SeqCrafter/LibreTV %&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&lt;!-- node 2021 年 2 月 11 日 --&gt;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">吧啦吧啦吧啦</span></span><br><span class="line"><span class="emphasis">&#123;% endtimeline %&#125;</span></span><br></pre></td></tr></table></figure><p><a href="/projects/">点我查看示例</a></p><blockquote><p>ghcard 的功能因为调用的人太多会无法正常显示，因此需要自行搭建，搭建的仓库在：<a href="https://github.com/anuraghazra/github-readme-stats">anuraghazra&#x2F;github-readme-stats</a></p></blockquote><h3 id="首页左侧边栏欢迎信息"><a href="#首页左侧边栏欢迎信息" class="headerlink" title="首页左侧边栏欢迎信息"></a>首页左侧边栏欢迎信息</h3><div class="tag-plugin tabs" align="center"id="tab_1"><div class="nav-tabs"><div class="tab active"><a href="#tab_1-1">效果</a></div><div class="tab"><a href="#tab_1-2">设置方法</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_1-1"><div class="tag-plugin image"><div class="image-bg" style="background:white;padding:16px;aspect-ratio:526/800;width:250px;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2025/12/30/40e7882492545e81ebb665f16d529e85.jpg" data-src="https://tncache1-f1.v3mh.com/image/2025/12/30/40e7882492545e81ebb665f16d529e85.jpg"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_1-2"><p>在目录（如果没有就创建一下）<code>source/_data/widgets.yml</code> 中添加以下组件即可自动生效</p><figure class="highlight yaml"><table><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 class="attr">welcome:</span></span><br><span class="line">  <span class="attr">layout:</span> <span class="string">markdown</span></span><br><span class="line">  <span class="attr">title:</span> <span class="string">欢迎,</span> <span class="string">路行者</span></span><br><span class="line">  <span class="attr">linklist:</span> <span class="comment"># 与 linklist 组件写法相同</span></span><br><span class="line">    <span class="attr">columns:</span> <span class="number">1</span></span><br><span class="line">    <span class="attr">items:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">icon:</span> <span class="string">solar:information</span> <span class="comment"># 记得去icons.yml中添加该图标，默认不存在！！</span></span><br><span class="line">        <span class="attr">title:</span> <span class="string">立即前往</span></span><br><span class="line">        <span class="attr">url:</span> <span class="string">/projects/#comments</span></span><br><span class="line">  <span class="attr">content:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    您已经到世界边缘，喜欢的话欢迎评论区讨论交流吧～</span></span><br><span class="line"><span class="string">    &gt; 如果想要添加友链，可以点击按钮去评论区留下你的链接，如果比较多的话，我开一个单独的页面.</span></span><br></pre></td></tr></table></figure></div></div></div><h3 id="首页右侧说说功能"><a href="#首页右侧说说功能" class="headerlink" title="首页右侧说说功能"></a>首页右侧说说功能</h3><div class="tag-plugin tabs" align="center"id="tab_2"><div class="nav-tabs"><div class="tab active"><a href="#tab_2-1">效果</a></div><div class="tab"><a href="#tab_2-2">设置方法</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_2-1"><div class="tag-plugin image"><div class="image-bg" style="background:white;padding:16px;aspect-ratio:600/700;width:350px;"><img class="lazy" src="https://tncache1-f1.v3mh.com/image/2025/12/30/1d4366fc8192d6d5af89d876f1f05055.jpg" data-src="https://tncache1-f1.v3mh.com/image/2025/12/30/1d4366fc8192d6d5af89d876f1f05055.jpg"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_2-2"><p>该功能基于 memos, 想要实现 memos 功能，首先需要注册一个账号。注册账号可以使用别人已经搭建好的 memos，也可以自行搭建 memos（需要有服务器，我就不折腾了）。这里直接采用的杜老师的 memos。</p><div class="tag-plugin link dis-select"><a class="link-card rich" title="使用 Memos 搭建时光机教程" href="https://dusays.com/561/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="icon"><div class="top"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div><span class="cap link footnote">https://dusays.com/561/</span></div><div class="bottom"><span class="title">使用 Memos 搭建时光机教程</span><span class="cap desc footnote">true</span></div></a></div><ul><li>查看<a href="https://dusays.com/561/">教程</a>，注册一个自己的账号</li><li>注册好后，在教程下面评论让杜老师帮忙查看自己的 <code>ID</code></li><li>首先在 <code>source\_data\widgets.yml</code> 中新增一条记录</li></ul><figure class="highlight yaml"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 哔哔</span></span><br><span class="line"><span class="attr">memos-du:</span></span><br><span class="line"><span class="attr">layout:</span> <span class="string">timeline</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">🥂哔哔广场</span></span><br><span class="line"><span class="attr">api:</span> <span class="string">https://s.dusays.com/api/v1/memo?creatorId=&#123;你的ID&#125;</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">memos</span></span><br><span class="line"><span class="attr">hide:</span> <span class="string">user,footer</span></span><br></pre></td></tr></table></figure><ul><li>然后在 <code>_config.stellar.yml</code> 设置让其生效</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">site_tree:</span></span><br><span class="line">  <span class="attr">home:</span></span><br><span class="line">    <span class="attr">rightbar:</span> <span class="string">memos-du</span></span><br></pre></td></tr></table></figure></div></div></div><h3 id="页脚信息"><a href="#页脚信息" class="headerlink" title="页脚信息"></a>页脚信息</h3><p>按照个人需求取消掉注释，并在<code>_config.stellar.yml</code> 设置让其生效即可。</p><details class="tag-plugin colorful folding" color="yellow" child="codeblock"><summary><p>内容很多, 点我查看</p></summary><div class="body"><figure class="highlight plaintext"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">######## Footer ########</span><br><span class="line">footer:</span><br><span class="line">  social:</span><br><span class="line">    # github:</span><br><span class="line">    #   icon: &#x27;&lt;img src=&quot;https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/08a41b181ce68.svg&quot;/&gt;&#x27;</span><br><span class="line">    #   url: /</span><br><span class="line">    # music:</span><br><span class="line">    #   icon: &#x27;&lt;img src=&quot;https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/3845874.svg&quot;/&gt;&#x27;</span><br><span class="line">    #   url: /</span><br><span class="line">    # unsplash:</span><br><span class="line">    #   icon: &#x27;&lt;img src=&quot;https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/3616429.svg&quot;/&gt;&#x27;</span><br><span class="line">    #   url: /</span><br><span class="line">    # comments:</span><br><span class="line">    #   icon: &#x27;&lt;img src=&quot;https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/942ebbf1a4b91.svg&quot;/&gt;&#x27;</span><br><span class="line">    #   url: /about/#comments</span><br><span class="line">  sitemap:</span><br><span class="line">    # - title: 博客</span><br><span class="line">    #   items:</span><br><span class="line">    #     - &#x27;[近期发布](/)&#x27;</span><br><span class="line">    #     - &#x27;[分类](/blog/categories/)&#x27;</span><br><span class="line">    #     - &#x27;[标签](/blog/tags/)&#x27;</span><br><span class="line">    #     - &#x27;[归档](/blog/archives/)&#x27;</span><br><span class="line">    # - title: 项目</span><br><span class="line">    #   items:</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    # - title: 社交</span><br><span class="line">    #   items:</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    # - title: 关于</span><br><span class="line">    #   items:</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">    #     - &#x27;[xxx](/)&#x27;</span><br><span class="line">  content: | # 支持 Markdown 格式</span><br><span class="line">    本站由 [&#123;author.name&#125;](/) 使用 [&#123;theme.name&#125; &#123;theme.version&#125;](&#123;theme.tree&#125;) 主题创建。</span><br><span class="line">    本博客所有文章除特别声明外，均采用 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 许可协议，转载请注明出处。</span><br><span class="line">  # 主题用户越多，开发者维护和更新的积极性就越高，如果您喜欢本主题，请在适当的位置显示主题信息和仓库链接以表支持。</span><br></pre></td></tr></table></figure> </div></details><h3 id="外观自定义"><a href="#外观自定义" class="headerlink" title="外观自定义"></a>外观自定义</h3><p>可以修改字体类型，大小等，但是要预先引入字体文件，在主题配置文件<code>_config.stellar.yml</code>中添加：</p><p>我这里分别添加了霞露文楷和 Maple Mono 字体，字体 CDN 来自</p><div class="tag-plugin link dis-select"><a class="link-card rich" title="中文网字计划" href="https://chinese-font.netlify.app/zh-cn/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="icon"><div class="top"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div><span class="cap link footnote">https://chinese-font.netlify.app/zh-cn/</span></div><div class="bottom"><span class="title">中文网字计划</span><span class="cap desc footnote">true</span></div></a></div><figure class="highlight yaml"><table><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></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;link</span> <span class="string">rel=&#x27;stylesheet&#x27;</span> <span class="string">href=&#x27;https://chinese-fonts-cdn.deno.dev/packages/lywkpmydb/dist/LXGWWenKaiScreen/result.css&#x27;</span> <span class="string">/&gt;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;link</span> <span class="string">rel=&#x27;stylesheet&#x27;</span> <span class="string">href=&#x27;https://chinese-fonts-cdn.deno.dev/packages/maple-mono-cn/dist/MapleMono-CN-Regular/result.css&#x27;</span> <span class="string">/&gt;</span></span><br></pre></td></tr></table></figure><p>然后继续在主题文件中修改样式设置：</p><figure class="highlight yaml"><table><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></pre></td><td class="code"><pre><span class="line"><span class="attr">style:</span></span><br><span class="line">  <span class="attr">prefers_theme:</span> <span class="string">light</span></span><br><span class="line">  <span class="attr">font-size:</span></span><br><span class="line">    <span class="attr">root:</span> <span class="string">17px</span></span><br><span class="line">    <span class="attr">body:</span> <span class="number">0.</span><span class="string">99rem</span></span><br><span class="line">    <span class="attr">code:</span> <span class="number">88</span><span class="string">%</span></span><br><span class="line">    <span class="attr">codeblock:</span> <span class="number">0.</span><span class="string">82rem</span></span><br><span class="line">  <span class="attr">font-family:</span></span><br><span class="line">    <span class="attr">body:</span> <span class="string">&quot;&#x27;LXGW WenKai Screen&#x27;, system-ui, &#x27;Microsoft Yahei&#x27;, &#x27;Segoe UI&#x27;, Arial, sans-serif&quot;</span></span><br><span class="line">    <span class="attr">code:</span> <span class="string">&quot;&#x27;Maple Mono CN&#x27;, Menlo, Monaco, Consolas, system-ui, monospace, sans-serif&quot;</span></span><br><span class="line">    <span class="attr">codeblock:</span> <span class="string">&quot;&#x27;Maple Mono CN&#x27;, Menlo, Monaco, Consolas, system-ui, monospace, sans-serif&quot;</span></span><br></pre></td></tr></table></figure><h3 id="让文章分类信息具有不同颜色并添加分享按钮"><a href="#让文章分类信息具有不同颜色并添加分享按钮" class="headerlink" title="让文章分类信息具有不同颜色并添加分享按钮"></a>让文章分类信息具有不同颜色并添加分享按钮</h3><p>依旧是在主题设置<code>_config.stellar.yml</code>中写入以下内容即可</p><figure class="highlight yaml"><table><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="attr">article:</span></span><br><span class="line">  <span class="attr">category_color:</span></span><br><span class="line">    <span class="string">&quot;生物信息&quot;</span><span class="string">:</span> <span class="string">&quot;#1BCDFC&quot;</span></span><br><span class="line">    <span class="string">...</span></span><br><span class="line">  <span class="attr">share:</span> [<span class="string">wechat</span>, <span class="string">weibo</span>, <span class="string">email</span>, <span class="string">link</span>]</span><br></pre></td></tr></table></figure><h3 id="添加评论系统"><a href="#添加评论系统" class="headerlink" title="添加评论系统"></a>添加评论系统</h3><p>评论系统采用 artalk 才能具有原主题比较和谐的外观，因此最好是选择 artalk。</p><p>首先在主题设置<code>_config.stellar.yml</code>中写入以下内容启用 artalk.</p><figure class="highlight yaml"><table><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="attr">comments:</span></span><br><span class="line">  <span class="attr">service:</span> <span class="string">artalk</span></span><br><span class="line">  <span class="attr">artalk:</span></span><br><span class="line">    <span class="attr">server:</span> <span class="comment"># 后端服务地址</span></span><br><span class="line">    <span class="attr">site:</span> <span class="comment"># 站点名称</span></span><br></pre></td></tr></table></figure><blockquote><p>artalk 需要自己搭建，从而获取服务端地址和站点名称，最简单的搭建方法是使用<a href="https://run.claw.cloud/">ClawCloud</a>, 因为这个平台有每月免费 5$的免费额度，用来搭建 artalk 足够了，另外其还提供了一键搭建的应用服务，在应用市场直接点击即可一键搭建。</p></blockquote>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;搭建过很多的个人博客，从 WordPress, Typecho 到 Halo，甚至是 Nuxt 自建的博客，但最终感觉都不方便，还是 Hexo 博客写起来更方便，托管也无需服务器。一开始尝试了 &lt;a</summary>
        
      
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>理解字符串匹配算法KMP</title>
    <link href="https://blog.xiaohanys.top/kmp-algorithm-julia/"/>
    <id>https://blog.xiaohanys.top/kmp-algorithm-julia/</id>
    <published>2025-12-24T13:16:37.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>字符串匹配问题的基本形式是：在文本串 text 中，查找模式串 pattern 出现的位置。最朴素的做法是暴力匹配：一旦发生不匹配，就把模式串整体右移一位，重新从头比较。<br>这种方法的问题在于：已经比较过的字符被重复比较, 最坏时间复杂度是 O(nm). KMP（Knuth–Morris–Pratt）算法的核心目标只有一个：当匹配失败时，模式串尽可能向右跳，而不是回到起点。</p><span id="more"></span><h3 id="理解-KMP-算法的过程"><a href="#理解-KMP-算法的过程" class="headerlink" title="理解 KMP 算法的过程"></a>理解 KMP 算法的过程</h3><p>举例来说，有一个字符串”BBC ABCDAB ABCDABCDABDE”，我想知道，里面是否包含另一个字符串”ABCDABD”？</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 1 ; q= 0</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">|</span><br><span class="line">ABCDABD</span><br></pre></td></tr></table></figure><p>首先，字符串”BBC ABCDAB ABCDABCDABDE”的第一个字符与搜索词”ABCDABD”的第一个字符进行比较。因为 B 与 A 不匹配，所以搜索词后移一位。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 2 ; q = 1</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line"> |</span><br><span class="line"> ABCDABD</span><br></pre></td></tr></table></figure><p>因为 B 与 A 不匹配，搜索词再往后移。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 5 ; q = 2</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">    |</span><br><span class="line">    ABCDABD</span><br></pre></td></tr></table></figure><p>就这样，直到字符串有一个字符，与搜索词的第一个字符相同为止。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 6 ; q = 3</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">     |</span><br><span class="line">    ABCDABD</span><br></pre></td></tr></table></figure><p>接着比较字符串和搜索词的下一个字符，还是相同。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 11 ; q = 6</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">          |</span><br><span class="line">    ABCDABD</span><br></pre></td></tr></table></figure><p>直到字符串有一个字符，与搜索词对应的字符不相同为止。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">     |</span><br><span class="line">     ABCDABD</span><br></pre></td></tr></table></figure><p>这时，最自然的反应是，将搜索词整个后移一位，再从头逐个比较。这样做虽然可行，但是效率很差，因为你要把”搜索位置”移到已经比较过的位置，重比一遍。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">          |</span><br><span class="line">    ABCDABD</span><br></pre></td></tr></table></figure><p>一个基本事实是，当空格与 D 不匹配时，你其实知道前面六个字符是”ABCDAB”。KMP 算法的想法是，设法利用这个已知信息，不要把”搜索位置”移回已经比较过的位置，继续把它向后移，这样就提高了效率。</p><table><thead><tr><th align="center">搜索词</th><th align="center">A</th><th align="center">B</th><th align="center">C</th><th align="center">D</th><th align="center">A</th><th align="center">B</th><th align="center">D</th></tr></thead><tbody><tr><td align="center">部分匹配值</td><td align="center">0</td><td align="center">0</td><td align="center">0</td><td align="center">0</td><td align="center">1</td><td align="center">2</td><td align="center">0</td></tr></tbody></table><p>怎么做到这一点呢？可以针对搜索词，算出一张《部分匹配表》（Partial Match Table）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">          |</span><br><span class="line">    ABCDABD</span><br></pre></td></tr></table></figure><p>已知空格与 D 不匹配时，前面六个字符”ABCDAB”是匹配的。查表可知，最后一个匹配字符 B 对应的”部分匹配值”为 2，因此按照下面的公式算出向后移动的位数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">移动位数 = 已匹配的字符数 - 对应的部分匹配值</span><br></pre></td></tr></table></figure><p>因为 6 - 2 等于 4，所以将搜索词向后移动 4 位。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 11; q = 2</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">          |</span><br><span class="line">        ABCDABD</span><br></pre></td></tr></table></figure><p>因为空格与Ｃ不匹配，搜索词还要继续往后移。这时，已匹配的字符数为 2（”AB”），对应的”部分匹配值”为 0。所以，移动位数 &#x3D; 2 - 0，结果为 2，于是将搜索词向后移 2 位。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 11; q = 0</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">          |</span><br><span class="line">          ABCDABD</span><br></pre></td></tr></table></figure><p>因为空格与 A 不匹配，继续后移一位。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 18; q = 2</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">                 |</span><br><span class="line">           ABCDABD</span><br></pre></td></tr></table></figure><p>逐位比较，直到发现 C 与 D 不匹配。于是，移动位数 &#x3D; 6 - 2，继续将搜索词向后移动 4 位。</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">### i = 18; q = 7</span></span><br><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">                     |</span><br><span class="line">               ABCDABD</span><br></pre></td></tr></table></figure><p>逐位比较，直到搜索词的最后一位，发现完全匹配，于是搜索完成。如果还要继续搜索（即找出全部匹配），移动位数 &#x3D; 7 - 0，再将搜索词向后移动 7 位，这里就不再重复了。</p><h3 id="部分匹配表的如何产生"><a href="#部分匹配表的如何产生" class="headerlink" title="部分匹配表的如何产生"></a>部分匹配表的如何产生</h3><table><thead><tr><th align="center">搜索词</th><th align="center">A</th><th align="center">B</th><th align="center">C</th><th align="center">D</th><th align="center">A</th><th align="center">B</th><th align="center">D</th></tr></thead><tbody><tr><td align="center">部分匹配值</td><td align="center">0</td><td align="center">0</td><td align="center">0</td><td align="center">0</td><td align="center">1</td><td align="center">2</td><td align="center">0</td></tr></tbody></table><p>“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例，</p><ul><li>“A”的前缀和后缀都为空集，共有元素的长度为<strong>0</strong>；</li><li>“AB”的前缀为<code>[A]</code>，后缀为<code>[B]</code>，共有元素的长度为<strong>0</strong>；</li><li>“ABC”的前缀为<code>[A, AB]</code>，后缀为<code>[BC, C]</code>，共有元素的长度<strong>0</strong>；</li><li>“ABCD”的前缀为<code>[A, AB, ABC]</code>，后缀为<code>[BCD, CD, D]</code>，共有元素的长度为<strong>0</strong>；</li><li>“ABCDA”的前缀为<code>[A, AB, ABC, ABCD]</code>，后缀为<code>[BCDA, CDA, DA, A]</code>，共有元素为<code>&quot;A&quot;</code>，长度为<strong>1</strong>；</li><li>“ABCDAB”的前缀为<code>[A, AB, ABC, ABCD, ABCDA]</code>，后缀为<code>[BCDAB, CDAB, DAB, AB, B]</code>，共有元素为<code>&quot;AB&quot;</code>，长度为<strong>2</strong>；</li><li>“ABCDABD”的前缀为<code>[A, AB, ABC, ABCD, ABCDA, ABCDAB]</code>，后缀为<code>[BCDABD, CDABD, DABD, ABD, BD, D]</code>，共有元素的长度为<strong>0</strong>。</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">BBC ABCDAB ABCDABCDABDE</span><br><span class="line">                 |</span><br><span class="line">           ABCDABD</span><br></pre></td></tr></table></figure><blockquote><p>“部分匹配”的实质是，有时候，字符串头部和尾部会有重复。比如，”ABCDAB”之中有两个”AB”，那么它的”部分匹配值”就是 2（”AB”的长度）。搜索词移动的时候，第一个”AB”向后移动 4 位（字符串长度-部分匹配值），就可以来到第二个”AB”的位置。</p></blockquote><div class="tag-plugin colorful note" color="red"><div class="body"><p>所以在写算法的过程中，我们无需回退查询字符串的坐标，一旦找到大于0的部分匹配值（注意，这里的部分匹配值其实指向的是重复字符串的最后一个字符，也就是说我们已经知晓它一定存在在查询字符串中），我们直接比较当前坐标对应的字符和部分匹配值对应的下一个字符是否匹配，然后循环回退（其实部分匹配值就是在模式字符串上不断回退的过程），最终直到找到完整的字符串。这样，整个寻找的流程的复杂度大大降低。其中构建部分匹配需要的复杂度为O(m)，而匹配过程的复杂度为O(n)，总复杂度为O(m+n)，相比较于朴素算法的O(mn)复杂度，KMP算法的效率大大提高。</p></div></div><h3 id="KMP-算法的实现"><a href="#KMP-算法的实现" class="headerlink" title="KMP 算法的实现"></a>KMP 算法的实现</h3><h4 id="构建部分匹配表"><a href="#构建部分匹配表" class="headerlink" title="构建部分匹配表"></a>构建部分匹配表</h4><p>根据前面的描述，我们可以手动构建匹配表，代码如下：</p><figure class="highlight julia"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> get_longest_intersaction(lst1::<span class="built_in">Set</span>&#123;<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;&#125;, lst2::<span class="built_in">Set</span>&#123;<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;&#125;)::<span class="built_in">Int</span></span><br><span class="line">    ints = intersect(lst1, lst2)</span><br><span class="line">    common_len = <span class="number">0</span></span><br><span class="line">    <span class="keyword">if</span> length(ints) == <span class="number">0</span></span><br><span class="line">        common_len = <span class="number">0</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        common_len = maximum(length.(ints))</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    common_len</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> partial_match_table_manually(pattern::<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;)::<span class="built_in">Vector</span>&#123;<span class="built_in">Int</span>&#125;</span><br><span class="line">    prefix, suffix = <span class="built_in">Set</span>&#123;<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;&#125;(), <span class="built_in">Set</span>&#123;<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;&#125;()</span><br><span class="line">    pLen = length(pattern)</span><br><span class="line">    Next = zeros(<span class="built_in">Int</span>, pLen)</span><br><span class="line">    push!(prefix, [pattern[<span class="number">1</span>]])</span><br><span class="line">    push!(suffix, [pattern[<span class="number">2</span>]])</span><br><span class="line">    ints = intersect(prefix, suffix)</span><br><span class="line">    common_len = get_longest_intersaction(prefix, suffix)</span><br><span class="line">    Next[<span class="number">2</span>] = common_len</span><br><span class="line">    <span class="keyword">for</span> i = <span class="number">3</span>:pLen</span><br><span class="line">        push!(prefix, pattern[<span class="number">1</span>:(i-<span class="number">1</span>)])</span><br><span class="line">        empty!(suffix)</span><br><span class="line">        <span class="keyword">for</span> j = <span class="number">2</span>:i</span><br><span class="line">            push!(suffix, pattern[j:i])</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">        ints = intersect(prefix, suffix)</span><br><span class="line">        common_len = get_longest_intersaction(prefix, suffix)</span><br><span class="line">        Next[i] = common_len</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    Next</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>当然，我们还有更简单的算法构建该表，代码如下：</p><figure class="highlight julia"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> partial_match_table(pattern::<span class="built_in">Vector</span>&#123;<span class="built_in">Char</span>&#125;)::<span class="built_in">Vector</span>&#123;<span class="built_in">Int</span>&#125;</span><br><span class="line">    pLen = length(pattern)</span><br><span class="line">    Next = zeros(<span class="built_in">Int</span>, pLen)</span><br><span class="line">    k = <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> q = <span class="number">2</span>:pLen</span><br><span class="line">        <span class="keyword">while</span> k &gt; <span class="number">0</span> &amp;&amp; pattern[k+<span class="number">1</span>] != pattern[q]</span><br><span class="line">            k = Next[k]</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">        <span class="keyword">if</span> pattern[k+<span class="number">1</span>] == pattern[q]</span><br><span class="line">            k += <span class="number">1</span></span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">        Next[q] = k</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    Next</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>这个构建方法的原理和 KMP 算法的匹配过程是相同的，都是通过回退寻找部分匹配值。具体请见 <a href="https://blog.csdn.net/qingdujun/article/details/85281936">KMP 算法：线性时间 O(n)字符串匹配算法</a>。</p><h4 id="KMP-算法的匹配过程"><a href="#KMP-算法的匹配过程" class="headerlink" title="KMP 算法的匹配过程"></a>KMP 算法的匹配过程</h4><p>根据前面对算法的理解，我们的代码如下：</p><figure class="highlight julia"><table><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><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> KMP_match(text::T, pattern::T)::<span class="built_in">Vector</span>&#123;<span class="built_in">Int</span>&#125; <span class="keyword">where</span> &#123;T&lt;:<span class="built_in">AbstractString</span>&#125;</span><br><span class="line">    tLen, pLen = length(text), length(pattern)</span><br><span class="line">    text_lst = collect(text)</span><br><span class="line">    pattern_lst = collect(pattern)</span><br><span class="line">    Next = partial_match_table(pattern_lst)</span><br><span class="line">    q = <span class="number">0</span></span><br><span class="line">    idxs = <span class="built_in">Int</span>[]</span><br><span class="line">    <span class="keyword">for</span> i = <span class="number">1</span>:tLen</span><br><span class="line">        <span class="comment">#println(&quot;i = $i&quot;)</span></span><br><span class="line">        <span class="keyword">while</span> q &gt; <span class="number">0</span> &amp;&amp; pattern_lst[q+<span class="number">1</span>] != text_lst[i]</span><br><span class="line">            q = Next[q]</span><br><span class="line">            <span class="comment">#println(&quot;q_while = $q&quot;)</span></span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">        <span class="keyword">if</span> pattern_lst[q+<span class="number">1</span>] == text_lst[i]</span><br><span class="line">            q += <span class="number">1</span></span><br><span class="line">            <span class="comment">#println(&quot;q_if = $q&quot;)</span></span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">        <span class="keyword">if</span> q == pLen</span><br><span class="line">            push!(idxs, i-pLen+<span class="number">1</span>)</span><br><span class="line">            q = <span class="number">0</span></span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    idxs</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>你可以手动取消掉<code>println</code>的注释，打印出每个循环的数值，来帮助你理解这个算法。另外，我已经在前面的详解步骤中添加了此刻<code>i</code> 和 <code>q</code> 的值，你可以和代码中的值对应起来。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;字符串匹配问题的基本形式是：在文本串 text 中，查找模式串 pattern 出现的位置。最朴素的做法是暴力匹配：一旦发生不匹配，就把模式串整体右移一位，重新从头比较。&lt;br&gt;这种方法的问题在于：已经比较过的字符被重复比较, 最坏时间复杂度是 O(nm). KMP（Knuth–Morris–Pratt）算法的核心目标只有一个：当匹配失败时，模式串尽可能向右跳，而不是回到起点。&lt;/p&gt;</summary>
    
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/categories/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/tags/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
    <category term="julia" scheme="https://blog.xiaohanys.top/tags/julia/"/>
    
    <category term="算法" scheme="https://blog.xiaohanys.top/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>我用nextjs写了个绘画web页面</title>
    <link href="https://blog.xiaohanys.top/next-ai-web/"/>
    <id>https://blog.xiaohanys.top/next-ai-web/</id>
    <published>2025-06-12T07:23:43.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>我们看 Silicon Cloud 官方文档也好，模型仓库也好都把那几个绘画模型给移除了，例如 SD3.5, Flux 等，但其实 API 还在，于是我参考 CherryStudio 软件里调用的方式，写了个 web 页面，可以调用 API 进行绘画，算是将这个功能找回了吧。</p><span id="more"></span><p>仓库的地址在这里：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://github.com/panxiaoguang/AI_Paint_Silicon" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://github.com/panxiaoguang/AI_Paint_Silicon</span><span class="cap link footnote">https://github.com/panxiaoguang/AI_Paint_Silicon</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>虽然是用了 nextjs, 但是因为绘画基本上全都是在调用 API 和交互式表单，所以用的都是客户端渲染。只是把调用 API 的代码和用户认证鉴权的代码放到了后端执行。</p><p>本人就看了两天的 nextjs 的文档，没有任何前端开发经验，但是就写出来了，这非常感谢 AI 时代大模型的帮助。</p><p>其中最复杂的用户认证，因为没有数据库，我就只是把用户名和密码硬编码写到了代码里，所以你只需要把你想用来登录的账号和密码写到环境变量就可以了，然后再提供 API,基本整个过程是非常安全的，只要是你得用户名和密码没有被泄露！</p><p>哦，用户认证的代码是 claude 帮我写的。前端交互式用的 HeroUI 写的。</p><p>用法的话，就直接 fork 到自己的仓库，然后和 vercel 链接，把 readme 里的几个环境变量写好就 OK 了，</p><p>第一次打开页面，点一些生成按钮，就会跳转到登录页面了，登录好了，后面 24 小时你的 cookie 不会过期，就不用重新登录了。</p><p>我部署的例子：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://silicondraw.xiaohanys.top/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://silicondraw.xiaohanys.top/</span><span class="cap link footnote">https://silicondraw.xiaohanys.top/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;我们看 Silicon Cloud 官方文档也好，模型仓库也好都把那几个绘画模型给移除了，例如 SD3.5, Flux 等，但其实 API 还在，于是我参考 CherryStudio 软件里调用的方式，写了个 web 页面，可以调用 API 进行绘画，算是将这个功能找回了吧。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>用Robyn,HTMX以及alpine.js创建一个AI绘画页面</title>
    <link href="https://blog.xiaohanys.top/robyn-htmx-alpinejs/"/>
    <id>https://blog.xiaohanys.top/robyn-htmx-alpinejs/</id>
    <published>2025-06-06T04:38:37.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>最近一直关注网站搭建的相关讯息，前面学习了 Reflex 框架，其核心是通过 FastAPI 作为后端，然后前端通过 nextjs 渲染静态页面并调取后端数据完成交互，是一个开箱即用的包。但是我在使用过程中也发现了一个问题，那就是所有的交互都依赖于后端，如果网络连接不顺畅，或者你距离后端服务器太远，那么用户的交互是非常卡顿的。</p><span id="more"></span><div class="tag-plugin colorful note" color="blue"><div class="title">我举个简单的例子，一个滑动组件，当用户滑动的时候，前面页面需要实时显示滑动到的数字，如果使用Reflex,</div><div class="body"><p>那么过程是：前端滑动到一个位置，把数据发送到后端，后端计算出新的数据，更新到前端（基于 WebSocket）。但其实这个过程完全不需要进入后端的，例如Vue的数据双向绑定，直接前端完成整个过程会相对更加流畅。</p></div></div><p>最近，我发现了几个轻量化的库，一个是<a href="https://robyn.tech/">Robyn</a>, 这个库是一个基于 Rust 的 Http 服务器，其性能很强，据官网测评是远强于 FastAPI 的。而且相比于 FastAPI, Robyn 更简单，文档就几页，拿来就可以用。<br>对于前端的交互，<a href="https://alpinejs.dev/">Alpine.js</a>则是 VueJS 的替代，我们既然使用了 Python 来搭建网站，那么最好就是尽量少碰前端框架，像 Vuejs 或者 React 更倾向于用前端的技术栈来搭建全栈服务，这意味着你要起一个 nodejs 的服务和后端交互，这无疑增加了复杂度。</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/06/06/205d12c81c00a90679f195de9a071c21.png" alt="image"></p><p>Alpine.js 则是一个轻量级的库，其核心是使用 JavaScript 来完成前端的交互，我们设置只需要添加一行 CDN 在我们的 html 文件里就可以使用它的功能了。虽然 Alpine.js 可以使用 Javascript 来使用 Ajax 获取后端数据，但是<a href="https://htmx.org/">HTMX</a>则更简单，我们甚至不需要写太多的 JS 代码即可以完成页面的交互以及数据的获取。那么接下来，我们用一个简单的例子来看看如何整个这三个库，来创建一个 AI 绘画页面。</p><h2 id="创建一个-AI-绘画页面"><a href="#创建一个-AI-绘画页面" class="headerlink" title="创建一个 AI 绘画页面"></a>创建一个 AI 绘画页面</h2><h3 id="1-创建一个-Robyn-项目"><a href="#1-创建一个-Robyn-项目" class="headerlink" title="1. 创建一个 Robyn 项目"></a>1. 创建一个 Robyn 项目</h3><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line">$ python3 -m venv .venv</span><br><span class="line">$ <span class="built_in">source</span> .venv/bin/activate</span><br><span class="line">$ pip install robyn</span><br><span class="line">$ python -m robyn --create</span><br></pre></td></tr></table></figure><p>这会开启一个交互式对话，我们一次选择即可创建一个项目：</p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line">$ python3 -m robyn --create</span><br><span class="line">? Directory Path: .</span><br><span class="line">? Need Docker? (Y/N) Y</span><br><span class="line">? Please <span class="keyword">select</span> project <span class="built_in">type</span> (Mongo/Postgres/Sqlalchemy/Prisma):</span><br><span class="line">❯ No DB</span><br><span class="line">  Sqlite</span><br><span class="line">  Postgres</span><br><span class="line">  MongoDB</span><br><span class="line">  SqlAlchemy</span><br><span class="line">  Prisma</span><br></pre></td></tr></table></figure><h3 id="2-用-AI-帮我们写一个纯-HTML-的前端页面"><a href="#2-用-AI-帮我们写一个纯-HTML-的前端页面" class="headerlink" title="2. 用 AI 帮我们写一个纯 HTML 的前端页面"></a>2. 用 AI 帮我们写一个纯 HTML 的前端页面</h3><p>我们像 Gemini Pro 2.5 提问：</p><blockquote><p>使用 DaisyUI 创建一个 AI 绘画的 HTML 页面，该页面包含 Navbar, Body 和 Footer, Body 分为两栏，分为 Sidebar 和 View aera, 其中 SideBar 负责收集用户提交的参数（Propmt, Steps, Button）,而 View aera 则负责画面呈现，要求生成的图可以被用户点击放大预览。要求整个页面的 UI 现代化，简约但不失美观。”</p></blockquote><p>然后，我们创建一个 templates 的目录，并创建一个 index.html 文件，把 AI 生成的代码粘贴进去。</p><figure class="highlight bash"><table><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">$ <span class="built_in">mkdir</span> -p templates</span><br><span class="line">$ <span class="built_in">touch</span> templates/index.html</span><br></pre></td></tr></table></figure><p>我们得到的 html 文件如下：</p><details class="tag-plugin colorful folding" color="green"><summary><p>查看代码 codeblock:true</p></summary><div class="body"><figure class="highlight html"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;zh-CN&quot;</span> <span class="attr">data-theme</span>=<span class="string">&quot;light&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>AI 绘画<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* Google Font for a more modern look - optional */</span></span></span><br><span class="line"><span class="language-css">      <span class="keyword">@import</span> url(<span class="string">&quot;https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap&quot;</span>);</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: flex;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">flex-direction</span>: column;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">min-height</span>: <span class="number">100vh</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-family</span>: <span class="string">&quot;Inter&quot;</span>, sans-serif; <span class="comment">/* Apply modern font */</span></span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">linear-gradient</span>(</span></span><br><span class="line"><span class="language-css">          <span class="number">135deg</span>,</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--b2)) <span class="number">0%</span>,</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--b3)) <span class="number">100%</span></span></span><br><span class="line"><span class="language-css">        );</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.main-content-wrapper</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">flex-grow</span>: <span class="number">1</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.modal-image-preview</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">max-width</span>: <span class="number">90vw</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">max-height</span>: <span class="number">85vh</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">object-fit</span>: contain;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">0.5rem</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="comment">/* Custom scrollbar for prompt textarea (optional, but nice for aesthetics) */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.prompt-textarea</span>::-webkit-scrollbar &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.prompt-textarea</span>::-webkit-scrollbar-thumb &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background-color</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--bc) / <span class="number">0.4</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.prompt-textarea</span>::-webkit-scrollbar-track &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background-color</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--b1));</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-id">#generated_image_display</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">max-width</span>: <span class="number">100%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">max-height</span>: <span class="number">65vh</span>; <span class="comment">/* Adjust max height in main view area */</span></span></span><br><span class="line"><span class="language-css">        <span class="attribute">object-fit</span>: contain;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">0.5rem</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">10px</span> <span class="number">15px</span> -<span class="number">3px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.1</span>), <span class="number">0</span> <span class="number">4px</span> <span class="number">6px</span> -<span class="number">2px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.05</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: transform <span class="number">0.3s</span> ease;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-id">#generated_image_display</span><span class="selector-pseudo">:hover</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.02</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">      <span class="selector-id">#initial_placeholder_text</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--bc) / <span class="number">0.6</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* 侧边栏磨砂效果 */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.sidebar</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: width <span class="number">0.3s</span> ease-in-out;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">rgba</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, <span class="number">0.1</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">backdrop-filter</span>: <span class="built_in">blur</span>(<span class="number">20px</span>);</span></span><br><span class="line"><span class="language-css">        -webkit-<span class="attribute">backdrop-filter</span>: <span class="built_in">blur</span>(<span class="number">20px</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="built_in">rgba</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, <span class="number">0.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">8px</span> <span class="number">32px</span> <span class="number">0</span> <span class="built_in">rgba</span>(<span class="number">31</span>, <span class="number">38</span>, <span class="number">135</span>, <span class="number">0.37</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* 深色主题下的侧边栏 */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-attr">[data-theme=<span class="string">&quot;dark&quot;</span>]</span> <span class="selector-class">.sidebar</span>,</span></span><br><span class="line"><span class="language-css">      <span class="selector-attr">[data-theme=<span class="string">&quot;synthwave&quot;</span>]</span> <span class="selector-class">.sidebar</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="built_in">rgba</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, <span class="number">0.1</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">8px</span> <span class="number">32px</span> <span class="number">0</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.5</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* 自定义滑块样式 */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.custom-range</span> &#123;</span></span><br><span class="line"><span class="language-css">        -webkit-<span class="attribute">appearance</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">appearance</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">height</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">linear-gradient</span>(</span></span><br><span class="line"><span class="language-css">          to right,</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--p)) <span class="number">0%</span>,</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--p)) <span class="built_in">var</span>(--range-value, <span class="number">20%</span>),</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--bc) / <span class="number">0.2</span>) <span class="built_in">var</span>(--range-value, <span class="number">20%</span>),</span></span><br><span class="line"><span class="language-css">          <span class="built_in">hsl</span>(<span class="built_in">var</span>(--bc) / <span class="number">0.2</span>) <span class="number">100%</span></span></span><br><span class="line"><span class="language-css">        );</span></span><br><span class="line"><span class="language-css">        <span class="attribute">outline</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: all <span class="number">0.3s</span> ease;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.custom-range</span>::-webkit-slider-thumb &#123;</span></span><br><span class="line"><span class="language-css">        -webkit-<span class="attribute">appearance</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">appearance</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">height</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">50%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--p));</span></span><br><span class="line"><span class="language-css">        <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">3px</span> solid <span class="built_in">hsl</span>(<span class="built_in">var</span>(--b1));</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">2px</span> <span class="number">8px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: all <span class="number">0.3s</span> ease;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.custom-range</span>::-webkit-slider-thumb:hover &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">4px</span> <span class="number">12px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.3</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.custom-range</span>::-moz-range-thumb &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">height</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">50%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--p));</span></span><br><span class="line"><span class="language-css">        <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">3px</span> solid <span class="built_in">hsl</span>(<span class="built_in">var</span>(--b1));</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">2px</span> <span class="number">8px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: all <span class="number">0.3s</span> ease;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.custom-range</span>::-moz-range-thumb:hover &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.2</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">4px</span> <span class="number">12px</span> <span class="built_in">rgba</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0.3</span>);</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* 滑块轨道样式 */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.range-labels</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: flex;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">justify-content</span>: space-between;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">0.75rem</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--bc) / <span class="number">0.6</span>);</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-top</span>: <span class="number">0.5rem</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">0</span> <span class="number">0.25rem</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="comment">/* 主题切换优化 */</span></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.theme-toggle</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: all <span class="number">0.3s</span> ease;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.theme-toggle</span><span class="selector-pseudo">:checked</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background-color</span>: <span class="built_in">hsl</span>(<span class="built_in">var</span>(--p));</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">    </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">      <span class="attr">class</span>=<span class="string">&quot;navbar bg-base-100/80 backdrop-blur-md shadow-lg sticky top-0 z-50 border-b border-base-300/50&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;flex-1&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">a</span> <span class="attr">class</span>=<span class="string">&quot;btn btn-ghost text-xl normal-case&quot;</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">svg</span></span></span><br><span class="line"><span class="tag">            <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2000/svg&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">width</span>=<span class="string">&quot;24&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">height</span>=<span class="string">&quot;24&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">viewBox</span>=<span class="string">&quot;0 0 24 24&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">fill</span>=<span class="string">&quot;none&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke</span>=<span class="string">&quot;currentColor&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-width</span>=<span class="string">&quot;2&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linecap</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linejoin</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;inline-block mr-2&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">path</span></span></span><br><span class="line"><span class="tag">              <span class="attr">d</span>=<span class="string">&quot;M12 3c.302 0 .59.03.871.089A7.008 7.008 0 0 1 21 7a3 3 0 0 1-3 3h-1.26a4 4 0 1 0-7.48 0H8a3 3 0 0 1-3-3 7.008 7.008 0 0 1 7.129-3.911A3.979 3.979 0 0 0 12 3Z&quot;</span></span></span><br><span class="line"><span class="tag">            &gt;</span><span class="tag">&lt;/<span class="name">path</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">path</span></span></span><br><span class="line"><span class="tag">              <span class="attr">d</span>=<span class="string">&quot;M12 18c-3.5 0-6.243-2.594-6.921-6h13.842c-.678 3.406-3.421 6-6.921 6Z&quot;</span></span></span><br><span class="line"><span class="tag">            &gt;</span><span class="tag">&lt;/<span class="name">path</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br><span class="line">          AI 绘画工坊</span><br><span class="line">        <span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;flex-none&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">label</span> <span class="attr">class</span>=<span class="string">&quot;flex cursor-pointer gap-2 items-center&quot;</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">svg</span></span></span><br><span class="line"><span class="tag">            <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2000/svg&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">width</span>=<span class="string">&quot;20&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">height</span>=<span class="string">&quot;20&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">viewBox</span>=<span class="string">&quot;0 0 24 24&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">fill</span>=<span class="string">&quot;none&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke</span>=<span class="string">&quot;currentColor&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-width</span>=<span class="string">&quot;2&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linecap</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linejoin</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">path</span> <span class="attr">d</span>=<span class="string">&quot;M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">path</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">input</span></span></span><br><span class="line"><span class="tag">            <span class="attr">type</span>=<span class="string">&quot;checkbox&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;toggle theme-controller theme-toggle toggle-sm&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">id</span>=<span class="string">&quot;theme-toggle&quot;</span></span></span><br><span class="line"><span class="tag">          /&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">svg</span></span></span><br><span class="line"><span class="tag">            <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2000/svg&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">width</span>=<span class="string">&quot;20&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">height</span>=<span class="string">&quot;20&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">viewBox</span>=<span class="string">&quot;0 0 24 24&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">fill</span>=<span class="string">&quot;none&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke</span>=<span class="string">&quot;currentColor&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-width</span>=<span class="string">&quot;2&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linecap</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">stroke-linejoin</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;12&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;12&quot;</span> <span class="attr">r</span>=<span class="string">&quot;5&quot;</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">path</span></span></span><br><span class="line"><span class="tag">              <span class="attr">d</span>=<span class="string">&quot;M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4&quot;</span></span></span><br><span class="line"><span class="tag">            /&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;main-content-wrapper container mx-auto p-4 sm:p-6 lg:p-8&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;flex flex-col md:flex-row gap-6 lg:gap-8&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">aside</span></span></span><br><span class="line"><span class="tag">          <span class="attr">class</span>=<span class="string">&quot;md:w-[320px] lg:w-[380px] w-full sidebar p-6 rounded-xl space-y-6 flex-shrink-0&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">h2</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;text-2xl font-semibold mb-4 border-b border-base-300/50 pb-3 text-primary&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            创作参数</span><br><span class="line">          <span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span><br><span class="line"></span><br><span class="line">          <span class="tag">&lt;<span class="name">form</span> <span class="attr">class</span>=<span class="string">&quot;space-y-6&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;prompt&quot;</span> <span class="attr">class</span>=<span class="string">&quot;label&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text text-base font-medium&quot;</span></span></span><br><span class="line"><span class="tag">                  &gt;</span>魔法咒语 (Prompt) ✨&lt;/span</span><br><span class="line">                &gt;</span><br><span class="line">              <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">textarea</span></span></span><br><span class="line"><span class="tag">                <span class="attr">name</span>=<span class="string">&quot;prompt&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">id</span>=<span class="string">&quot;prompt&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;textarea textarea-bordered textarea-lg w-full h-36 prompt-textarea bg-base-100/50 backdrop-blur-sm&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">placeholder</span>=<span class="string">&quot;例如：一只戴着宇航员头盔的猫漂浮在宇宙中，背景是绚丽的星云，数字艺术&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">required</span></span></span><br><span class="line"><span class="tag">              &gt;</span><span class="tag">&lt;/<span class="name">textarea</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;prompt_error_msg&quot;</span> <span class="attr">class</span>=<span class="string">&quot;text-error text-sm mt-1 h-4&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">            <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;num_steps&quot;</span> <span class="attr">class</span>=<span class="string">&quot;label justify-between&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text text-base font-medium&quot;</span></span></span><br><span class="line"><span class="tag">                  &gt;</span>绘画步数 (Steps) 🖼️&lt;/span</span><br><span class="line">                &gt;</span><br><span class="line">                <span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">class</span>=<span class="string">&quot;label-text-alt text-lg font-semibold text-primary&quot;</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">id</span>=<span class="string">&quot;steps_value_display&quot;</span></span></span><br><span class="line"><span class="tag">                  &gt;</span>4&lt;/span</span><br><span class="line">                &gt;</span><br><span class="line">              <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">input</span></span></span><br><span class="line"><span class="tag">                <span class="attr">type</span>=<span class="string">&quot;range&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">name</span>=<span class="string">&quot;num_steps&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">min</span>=<span class="string">&quot;1&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">max</span>=<span class="string">&quot;8&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">value</span>=<span class="string">&quot;4&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">step</span>=<span class="string">&quot;1&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;custom-range w-full&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">id</span>=<span class="string">&quot;steps_input&quot;</span></span></span><br><span class="line"><span class="tag">              /&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;range-labels&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">span</span>&gt;</span>快速 (1)<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">span</span>&gt;</span>精细 (8)<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">            <span class="tag">&lt;<span class="name">button</span></span></span><br><span class="line"><span class="tag">              <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span></span></span><br><span class="line"><span class="tag">              <span class="attr">class</span>=<span class="string">&quot;btn btn-primary btn-lg w-full mt-4 group bg-gradient-to-r from-primary to-secondary border-0&quot;</span></span></span><br><span class="line"><span class="tag">            &gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;group-hover:scale-110 transition-transform duration-300&quot;</span></span></span><br><span class="line"><span class="tag">                &gt;</span>开始创作&lt;/span</span><br><span class="line">              &gt;</span><br><span class="line">              <span class="tag">&lt;<span class="name">svg</span></span></span><br><span class="line"><span class="tag">                <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2000/svg&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">class</span>=<span class="string">&quot;h-6 w-6 inline-block ml-2 group-hover:animate-pulse&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">fill</span>=<span class="string">&quot;none&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">viewBox</span>=<span class="string">&quot;0 0 24 24&quot;</span></span></span><br><span class="line"><span class="tag">                <span class="attr">stroke</span>=<span class="string">&quot;currentColor&quot;</span></span></span><br><span class="line"><span class="tag">              &gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">path</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">stroke-linecap</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">stroke-linejoin</span>=<span class="string">&quot;round&quot;</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">stroke-width</span>=<span class="string">&quot;2&quot;</span></span></span><br><span class="line"><span class="tag">                  <span class="attr">d</span>=<span class="string">&quot;M12 19l9 2-9-18-9 18 9-2zm0 0v-8&quot;</span></span></span><br><span class="line"><span class="tag">                /&gt;</span></span><br><span class="line">              <span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line"></span><br><span class="line">          <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">            <span class="attr">id</span>=<span class="string">&quot;loading_indicator&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;htmx-indicator text-center mt-4 space-y-2&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;loading loading-dots loading-lg text-primary&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;text-primary animate-pulse&quot;</span>&gt;</span>正在召唤灵感缪斯...<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">aside</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">main</span></span></span><br><span class="line"><span class="tag">          <span class="attr">class</span>=<span class="string">&quot;flex-grow bg-base-100/50 backdrop-blur-sm border border-base-300/50 p-6 rounded-xl shadow-xl flex flex-col justify-center items-center min-h-[60vh] lg:min-h-[70vh]&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">            <span class="attr">x-data</span></span></span><br><span class="line"><span class="tag">            <span class="attr">id</span>=<span class="string">&quot;view_area_image_container&quot;</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;w-full h-full flex justify-center items-center&quot;</span></span></span><br><span class="line"><span class="tag">            @<span class="attr">click</span>=<span class="string">&quot;my_modal_2.showModal()&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;initial_placeholder_text&quot;</span> <span class="attr">class</span>=<span class="string">&quot;text-xl text-center&quot;</span>&gt;</span></span><br><span class="line">              您的 AI 画作将在此惊艳登场！🚀</span><br><span class="line">            <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">            <span class="comment">&lt;!-- &lt;img id=&quot;generated_image_display&quot; src=&quot;./btygir.png&quot; alt=&quot;生成的图像&quot; class=&quot;cursor-pointer&quot;/&gt; --&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">main</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dialog</span> <span class="attr">id</span>=<span class="string">&quot;my_modal_2&quot;</span> <span class="attr">class</span>=<span class="string">&quot;modal&quot;</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">            <span class="attr">class</span>=<span class="string">&quot;modal-box w-11/12 max-w-5xl p-0 relative bg-transparent shadow-none&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">img</span></span></span><br><span class="line"><span class="tag">              <span class="attr">id</span>=<span class="string">&quot;modal_image_content&quot;</span></span></span><br><span class="line"><span class="tag">              <span class="attr">src</span>=<span class="string">&quot;&quot;</span></span></span><br><span class="line"><span class="tag">              <span class="attr">alt</span>=<span class="string">&quot;图像预览&quot;</span></span></span><br><span class="line"><span class="tag">              <span class="attr">class</span>=<span class="string">&quot;modal-image-preview mx-auto&quot;</span></span></span><br><span class="line"><span class="tag">            /&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">form</span> <span class="attr">method</span>=<span class="string">&quot;dialog&quot;</span> <span class="attr">class</span>=<span class="string">&quot;modal-backdrop&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">button</span>&gt;</span>close<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dialog</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">footer</span></span></span><br><span class="line"><span class="tag">      <span class="attr">class</span>=<span class="string">&quot;footer footer-center p-6 bg-base-300/80 backdrop-blur-md text-base-content mt-auto border-t border-base-300/50&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">aside</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span>&gt;</span></span><br><span class="line">          版权所有 © <span class="tag">&lt;<span class="name">span</span> <span class="attr">id</span>=<span class="string">&quot;current_year&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span> AI 绘画工坊 - 由先进的 AI</span><br><span class="line">          技术驱动</span><br><span class="line">        <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;text-xs opacity-70&quot;</span>&gt;</span>简约设计，无限创意<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">aside</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">footer</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure> </div></details><p>注意，为了下面的方便，我们直接将需要的库全部引入在<code>&lt;head&gt;</code>标签内：</p><figure class="highlight html"><table><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">link</span></span></span><br><span class="line"><span class="tag">  <span class="attr">href</span>=<span class="string">&quot;https://cdn.jsdelivr.net/npm/daisyui@5&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">type</span>=<span class="string">&quot;text/css&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span></span></span><br><span class="line"><span class="tag">  <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/htmx.org@2.0.4&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">integrity</span>=<span class="string">&quot;sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">crossorigin</span>=<span class="string">&quot;anonymous&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span></span></span><br><span class="line"><span class="tag">  <span class="attr">defer</span></span></span><br><span class="line"><span class="tag">  <span class="attr">src</span>=<span class="string">&quot;https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="3-用-Robyn-创建一个跟路由来托管该页面"><a href="#3-用-Robyn-创建一个跟路由来托管该页面" class="headerlink" title="3. 用 Robyn 创建一个跟路由来托管该页面"></a>3. 用 Robyn 创建一个跟路由来托管该页面</h3><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> robyn <span class="keyword">import</span> Robyn, serve_html</span><br><span class="line"><span class="keyword">import</span> pathlib</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"></span><br><span class="line">app = Robyn(__file__)</span><br><span class="line"><span class="comment">##我们执行的文件路径</span></span><br><span class="line">current_file_path = pathlib.Path(__file__).parent.resolve()</span><br><span class="line">html_path = os.path.join(current_file_path, <span class="string">&quot;templates&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">##只需要使用serve_html即可以将整个页面返回给用户，这里我们不用模板，因为用不到</span></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">index</span>():</span><br><span class="line">  <span class="keyword">return</span> serve_html(os.path.join(html_path, <span class="string">&quot;index.html&quot;</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    app.start(host=<span class="string">&quot;0.0.0.0&quot;</span>, port=<span class="number">8080</span>)</span><br></pre></td></tr></table></figure><h3 id="4-使用-Alpine-js-来完成前端的交互"><a href="#4-使用-Alpine-js-来完成前端的交互" class="headerlink" title="4. 使用 Alpine.js 来完成前端的交互"></a>4. 使用 Alpine.js 来完成前端的交互</h3><p>我们现在打开 <a href="http://localhost:8080/">http://localhost:8080</a>, 可以看到我们创建的页面。但是我们发现前端是没有交互的，例如切换主题的按钮无效，滑块滑动后，数值也不会变化。下面我们来通过添加一些标签就可以实现需要大量 JS 代码来实现的操作。</p><h4 id="主题切换优化"><a href="#主题切换优化" class="headerlink" title="主题切换优化"></a>主题切换优化</h4><p>DaisyUI 主题切换需要在跟组件添加一个属性<code>data-theme</code>，我们在<code>&lt;body&gt;</code>标签添加它，并让切换按钮可以控制他，这就是数据绑定。</p><p>Body 标签修改如下：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">body</span> <span class="attr">x-data</span>=<span class="string">&quot;&#123; theme: false &#125;&quot;</span> <span class="attr">:data-theme</span>=<span class="string">&quot;theme ? &#x27;dark&#x27; : &#x27;light&#x27;&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后，我们修改切换按钮的代码，添加一个<code>x-model=&quot;theme&quot;</code>属性，这样就可以实现主题的切换。</p><figure class="highlight html"><table><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"><span class="tag">&lt;<span class="name">input</span></span></span><br><span class="line"><span class="tag">  <span class="attr">x-model</span>=<span class="string">&quot;theme&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">type</span>=<span class="string">&quot;checkbox&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">class</span>=<span class="string">&quot;toggle theme-controller theme-toggle toggle-sm&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">id</span>=<span class="string">&quot;theme-toggle&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span></span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="green"><div class="body"><p>这里的逻辑是：<code>input</code>标签的值会绑定到<code>x-model=theme</code>的变量上，所以<code>input</code>的变化会改变X-data中的数据，然后通过<code>:data-theme=theme ? &#39;dark&#39; : &#39;light&#39;</code>属性来控制主题。</p></div></div><h4 id="滑块交互优化"><a href="#滑块交互优化" class="headerlink" title="滑块交互优化"></a>滑块交互优化</h4><p>对于滑动组件，我们首先添加一个<code>x-data=&quot;&#123; steps: 4 &#125;</code>标签。</p><figure class="highlight html"><table><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">x-data</span>=<span class="string">&quot;&#123; steps: 4 &#125;&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;num_steps&quot;</span> <span class="attr">class</span>=<span class="string">&quot;label justify-between&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text text-base font-medium&quot;</span>&gt;</span>绘画步数 (Steps) 🖼️<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">      <span class="attr">class</span>=<span class="string">&quot;label-text-alt text-lg font-semibold text-primary&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">id</span>=<span class="string">&quot;steps_value_display&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后是 input 标签，添加一个<code>x-model=&quot;steps&quot;</code>属性，这样就可以实现滑块的交互。</p><figure class="highlight html"><table><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">input</span></span></span><br><span class="line"><span class="tag">  <span class="attr">x-model</span>=<span class="string">&quot;steps&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">type</span>=<span class="string">&quot;range&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">name</span>=<span class="string">&quot;num_steps&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">min</span>=<span class="string">&quot;1&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">max</span>=<span class="string">&quot;8&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">value</span>=<span class="string">&quot;4&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">step</span>=<span class="string">&quot;1&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">class</span>=<span class="string">&quot;custom-range w-full&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">id</span>=<span class="string">&quot;steps_input&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span></span><br></pre></td></tr></table></figure><p>最后在<code>&lt;span&gt;</code>使用<code>x-text=&quot;steps&quot;</code>来绑定滑块的值。</p><figure class="highlight html"><table><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">x-data</span>=<span class="string">&quot;&#123; steps: 4 &#125;&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;num_steps&quot;</span> <span class="attr">class</span>=<span class="string">&quot;label justify-between&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text text-base font-medium&quot;</span>&gt;</span>绘画步数 (Steps) 🖼️<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span></span></span><br><span class="line"><span class="tag">      <span class="attr">class</span>=<span class="string">&quot;label-text-alt text-lg font-semibold text-primary&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">id</span>=<span class="string">&quot;steps_value_display&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">x-text</span>=<span class="string">&quot;steps&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="green"><div class="body"><p>这里的原理就是：<code>x-data</code>标签会创建一个数据对象，<code>x-model=steps</code>会绑定到这个数据对象的<code>steps</code>属性，<code>x-text=steps</code>会绑定到这个数据对象的<code>steps</code>属性。</p></div></div><h3 id="5-使用-HTMX-来完成后端的交互"><a href="#5-使用-HTMX-来完成后端的交互" class="headerlink" title="5. 使用 HTMX 来完成后端的交互"></a>5. 使用 HTMX 来完成后端的交互</h3><p>这个就很简单了，我们假设需要向后端<code>/generate</code>路由发送<code>Post</code>请求，并获取返回的图片，那么我们只需要在<code>&lt;form&gt;</code>标签添加一个<code>hx-post</code>属性，并添加一个<code>hx-target</code>属性，这样就可以实现后端的交互。</p><figure class="highlight html"><table><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="tag">&lt;<span class="name">form</span></span></span><br><span class="line"><span class="tag">  <span class="attr">id</span>=<span class="string">&quot;generate_form&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">hx-post</span>=<span class="string">&quot;/generate&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">hx-target</span>=<span class="string">&quot;#initial_placeholder_text&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">hx-indicator</span>=<span class="string">&quot;#loading_indicator&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">hx-swap</span>=<span class="string">&quot;outerHTML&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">class</span>=<span class="string">&quot;space-y-6&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;prompt&quot;</span> <span class="attr">class</span>=<span class="string">&quot;label&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text text-base font-medium&quot;</span>&gt;</span>魔法咒语 (Prompt) ✨<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">textarea</span></span></span><br><span class="line"><span class="tag">      <span class="attr">name</span>=<span class="string">&quot;prompt&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">id</span>=<span class="string">&quot;prompt&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">class</span>=<span class="string">&quot;textarea textarea-bordered textarea-lg w-full h-36 prompt-textarea bg-basebackdrop-blur-sm&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">placeholder</span>=<span class="string">&quot;例如：一只戴着宇航员头盔的猫漂浮在宇宙中，背景是绚丽的星云，数字艺术&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">required</span></span></span><br><span class="line"><span class="tag">    &gt;</span><span class="tag">&lt;/<span class="name">textarea</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span> <span class="attr">id</span>=<span class="string">&quot;prompt_error_msg&quot;</span> <span class="attr">class</span>=<span class="string">&quot;text-error text-sm mt-1 h-4&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br></pre></td></tr></table></figure><div class="tag-plugin blockquote" indent="undefined"><p>我们依次解释一下含义：</p><ul><li><code>hx-post</code>：指定要发送请求的 URL</li><li><code>hx-target</code>：指定要更新的目标元素</li><li><code>hx-indicator</code>：指定要显示的加载指示器,在请求过程中会显示 loading 状态，请求完成后会隐藏</li><li><code>hx-swap</code>：指定要更新的方式</li></ul><p>这里我们使用<code>hx-swap=&quot;outerHTML&quot;</code>，这意味着当后端返回数据时，会替换掉<code>&lt;div&gt;</code>标签的内容。</p></div><h3 id="6-使用-Robyn-来创建后端路由"><a href="#6-使用-Robyn-来创建后端路由" class="headerlink" title="6. 使用 Robyn 来创建后端路由"></a>6. 使用 Robyn 来创建后端路由</h3><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">import</span> httpx</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_flux_image_data</span>(<span class="params">prompt, num_steps</span>):</span><br><span class="line">    endpoint = <span class="string">&quot;https://api.cloudflare.com/client/v4/accounts/&#123;accountid&#125;/ai/run/@cf/black-forest-labs/flux-1-schnell&quot;</span></span><br><span class="line">    headers =  &#123;</span><br><span class="line">            <span class="string">&quot;Authorization&quot;</span>: <span class="string">&quot;Bearer &#123;key&#125;&quot;</span>,</span><br><span class="line">            <span class="string">&quot;Content-Type&quot;</span>: <span class="string">&quot;application/json&quot;</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(num_steps, <span class="built_in">str</span>):</span><br><span class="line">        num_steps = <span class="built_in">int</span>(num_steps)</span><br><span class="line"></span><br><span class="line">    payload = &#123;</span><br><span class="line">        <span class="string">&quot;prompt&quot;</span>: prompt,</span><br><span class="line">        <span class="string">&quot;num_steps&quot;</span>: num_steps</span><br><span class="line">    &#125;</span><br><span class="line">    response = httpx.post(</span><br><span class="line">        endpoint, headers=headers, json=payload, timeout=<span class="literal">None</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> response.status_code == <span class="number">400</span>:</span><br><span class="line">        <span class="comment"># raise error</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        image_data = response.json()[<span class="string">&#x27;result&#x27;</span>][<span class="string">&#x27;image&#x27;</span>]</span><br><span class="line">        <span class="keyword">return</span> image_data</span><br></pre></td></tr></table></figure><p>首先，我们创建一个函数来代理 CF 的 API，这样好处是我们的密钥是保存在后端服务器的，不会暴露给前端。</p><p>然后，我们创建一个路由来处理请求：</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> robyn <span class="keyword">import</span> html</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> parse_qs</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/generate&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">generate</span>(<span class="params">request</span>):</span><br><span class="line">    body = parse_qs(request.body)</span><br><span class="line">    prompt = body[<span class="string">&quot;prompt&quot;</span>][<span class="number">0</span>]</span><br><span class="line">    num_steps = body[<span class="string">&quot;num_steps&quot;</span>][<span class="number">0</span>]</span><br><span class="line">    image_data = get_flux_image_data(prompt, num_steps)</span><br><span class="line">    html_str = <span class="string">f&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">&lt;img id=\&quot;generated_image_display\&quot; src=\&quot;data:image/png;base64,<span class="subst">&#123;image_data&#125;</span>\&quot; alt=\&quot;生成的图像\&quot; class=\&quot;cursor-pointer\&quot;/&gt;</span></span><br><span class="line"><span class="string">&lt;img hx-swap-oob=&quot;true&quot; id=&quot;modal_image_content&quot; src=&quot;data:image/png;base64,<span class="subst">&#123;image_data&#125;</span>&quot; alt=&quot;图像预览&quot; class=&quot;modal-image-preview mx-auto&quot;/&gt;</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> html(html_str)</span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="green"><div class="title">我们看到，我们的接口返回的是html的字符串，而不是通常的JSON数据，这是因为html期望我们返回的是html字符串，而不是JSON数据，并且用返回的字符串去替换`hx-target`指定的元素。</div><div class="body"><p>另外，这里用到了<code>hx-swap-oob</code>属性，这个属性的作用可以帮助我们在替换掉需要替换的元素后，可以顺便将<code>id</code>为<code>modal_image_content</code>的组件也替换掉，因为我们前端有个点击生成的图片会放大预览的效果，我们使用的是模态框，需要同步替换里面的内容。</p></div></div><h2 id="最终呈现："><a href="#最终呈现：" class="headerlink" title="最终呈现："></a>最终呈现：</h2><p>我们运行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ python3 -m robyn app.py <span class="comment">##假如我们的文件名是app.py</span></span><br></pre></td></tr></table></figure><p>然后我们打开 <a href="http://localhost:8080/">http://localhost:8080</a>, 就可以看到我们创建的页面了。</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/06/06/cd04e3afd7dd5b84c58e7caf0f550252.png" alt="image"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我们通过使用 Robyn, HTMX 以及 Alpine.js 创建了一个 AI 绘画页面，整个过程非常简单，只需要几个步骤就可以完成。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近一直关注网站搭建的相关讯息，前面学习了 Reflex 框架，其核心是通过 FastAPI 作为后端，然后前端通过 nextjs 渲染静态页面并调取后端数据完成交互，是一个开箱即用的包。但是我在使用过程中也发现了一个问题，那就是所有的交互都依赖于后端，如果网络连接不顺畅，或者你距离后端服务器太远，那么用户的交互是非常卡顿的。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="后端" scheme="https://blog.xiaohanys.top/tags/%E5%90%8E%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>免费搭建个人的云端Chat应用!</title>
    <link href="https://blog.xiaohanys.top/free-host-ai-chat/"/>
    <id>https://blog.xiaohanys.top/free-host-ai-chat/</id>
    <published>2025-05-29T22:29:22.310Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>如今 AI 大语言模型内卷严重，各种大语言模型层出不穷，今天 gpt 的生图模型很强大，明天 Gemini-pro-2.5 又突破天际，后天 Claude 4.0 又横空出世。用户可能需要不断的去开通各个大模型的服务，极度浪费金钱。那么最简单的方法或许就是使用 API 服务商提供的服务了，一个 API 可以用到所有的模型，而且按量付费，不用不花钱。</p><span id="more"></span><p>总结一下 API 的优点如下：</p><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>按量付费，避免无效浪费</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>一个API访问所有大模型</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>国内直接访问，没有网络问题</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>还可以调用API到vscode实现代码自动补充的功能，例如使用continue 插件</span></div><p>前面的文章已经详细的介绍了我用过的几家服务商，点击查看：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://blog.xiaohanys.xyz/AI-share1/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://blog.xiaohanys.xyz/AI-share1/</span><span class="cap link footnote">https://blog.xiaohanys.xyz/AI-share1/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>好了，回顾完了开始正题，调用 API 我们是需要一个云端的对话界面的，云端的好处就是部署好了之后，你可以在任意的设备（手机，电脑）都可以使用。</p><p>我推荐使用 librechat, 搭建好的界面如下：</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/50ebe2485e2df41e7f99337d9da884a3.png" alt="image"></p><p>可以看到整个界面和 ChatGPT 高度相似，沉浸式体验！！下面介绍如何免费云端搭建：</p><h2 id="数据持久化"><a href="#数据持久化" class="headerlink" title="数据持久化"></a>数据持久化</h2><p>我们需要让我们的聊天记录永久保存，所以需要一个数据库来存放它们。我推荐使用 MongoDB，因为 MongoDB 官方有提供免费的云端数据库服务，而且随意注册账号即可以使用（无身份验证），接下来我们首先前往<a href="https://account.mongodb.com/account/register">这里</a>注册一个账号。</p><h3 id="新建一个-Project"><a href="#新建一个-Project" class="headerlink" title="新建一个 Project"></a>新建一个 Project</h3><p>设置帐户后，单击左上角项目那里的“新建项目”按钮并为其命名（例如“LibreChat”）。</p><h3 id="建立数据库"><a href="#建立数据库" class="headerlink" title="建立数据库"></a>建立数据库</h3><p>点击“Create cluster” 按钮新建一个数据库，然后选择“Shared Clusters”选项，即免费的数据库。为您的 cluster 命名（例如“LibreChat-Cluster”），然后单击“Create deployment”。</p><h3 id="设置数据库凭证"><a href="#设置数据库凭证" class="headerlink" title="设置数据库凭证"></a>设置数据库凭证</h3><ol><li>单击侧栏中的“Database Access”选项。</li><li>单击“Add New Database User”按钮。</li><li>输入用户名和安全密码，然后单击“Add User”。</li></ol><h3 id="配置网络访问"><a href="#配置网络访问" class="headerlink" title="配置网络访问"></a>配置网络访问</h3><ol><li>点击侧栏中的“Network Access”选项。</li><li>点击“Add IP Address”按钮。</li><li>选择“Allow Access from Anywhere”，然后点击“确认”。</li></ol><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/42c87448c8eedbacba96fac04e44bb8f.png" alt="image"></p><h3 id="获取连接字符串"><a href="#获取连接字符串" class="headerlink" title="获取连接字符串"></a>获取连接字符串</h3><p>点击左侧的“Cluster”按钮，点击中间的“Connect”按钮，再点击“Drivers”, 随便选一个，例如 Python,你将会看到一段类似与这样的字符串：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mongodb+srv://xiaohanys:&lt;db_password&gt;@librechat-cluster.pazwvgb.mongodb.net/?retryWrites=<span class="literal">true</span>&amp;w=majority&amp;appName=LibreChat-Cluster</span><br></pre></td></tr></table></figure><p>将这个字符串修改成下面的类型：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mongodb+srv://username:password@librechat-cluster.pazwvgb.mongodb.net/LibreChat?retryWrites=<span class="literal">true</span></span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="yellow"><div class="title">注意，你需要在根链接后添加你需要的数据库名称，例如LibreChat,</div><div class="body"><p>否则会使用系统默认的test数据库来保存你的数据</p></div></div><h2 id="正式部署"><a href="#正式部署" class="headerlink" title="正式部署"></a>正式部署</h2><h3 id="注册-Hugging-face-并复制应用"><a href="#注册-Hugging-face-并复制应用" class="headerlink" title="注册 Hugging face 并复制应用"></a>注册 Hugging face 并复制应用</h3><p>Hugging face 提供了免费的 Space 服务，拥有免费的 2 核心 16G 内存的服务器，性能非常好，虽然 hugging face 国内无法访问，<strong>但是 Space 服务却可以流畅访问</strong>。</p><ol><li>登录或创建 <a href="https://huggingface.co/">Hugging Face</a> 账户</li><li>访问 <a href="https://huggingface.co/spaces/LibreChat/template">https://huggingface.co/spaces/LibreChat/template</a> 并单击 Duplicate this Space 将 LibreChat 模板复制到您的个人空间中。</li></ol><div class="tag-plugin colorful note" ><div class="body"><p>warning::注意：此模板出现运行时错误是正常的，您必须使用以下指南对其进行配置才能使其正常运行。</p></div></div><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/1e8e46f3baebdea9847746a683315aae.png" alt="image"></p><ol start="3"><li>命名你的空间并填写 Secrets 和 Variables</li></ol><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/e11956a66527159ae7ac89a1d94cc37d.png" alt="image"></p><p>你需要填写以下 Secrets 值：</p><table><thead><tr><th>Secrets</th><th>Values</th></tr></thead><tbody><tr><td>CREDS_IV</td><td>手动创建,见下文</td></tr><tr><td>CREDS_KEY</td><td>手动创建,见下文</td></tr><tr><td>JWT_REFRESH_SECRET</td><td>手动创建,见下文</td></tr><tr><td>JWT_SECRET</td><td>手动创建,见下文</td></tr><tr><td>MONGO_URI</td><td>上一步获取的数据库链接字符串</td></tr></tbody></table><p>使用<a href="https://www.librechat.ai/toolkit/creds_generator">这个</a>页面来生成需要手动创建的四个变量，依次填入即可</p><p>另外，你还需要填写 Variables 的值</p><table><thead><tr><th>Variables</th><th>Values</th></tr></thead><tbody><tr><td>APP_TITLE</td><td>你得标题</td></tr><tr><td>ALLOW_REGISTRATION</td><td>建议为 false</td></tr><tr><td>ALLOW_SOCIAL_LOGIN</td><td>true&#x2F;false</td></tr><tr><td>ALLOW_SOCIAL_REGISTRATION</td><td>true&#x2F;false</td></tr><tr><td>ALLOW_UNVERIFIED_EMAIL_LOGIN</td><td>务必为 true</td></tr><tr><td>ALLOW_PASSWORD_RESET</td><td>务必为 false</td></tr><tr><td>CONFIG_PATH</td><td>手动填写见下文</td></tr></tbody></table><p>如果你得<code>ALLOW_UNVERIFIED_EMAIL_LOGIN</code>为 false, 那么你需要验证邮箱才可以登录，需要额外配置邮箱服务，这里我们设置 true 后避免了这一个复杂的操作，而且就我们个人使用，所以创建一个自己的账户即可，<code>ALLOW_PASSWORD_RESET</code>必须设置为 false, 因为搭配上<code>ALLOW_UNVERIFIED_EMAIL_LOGIN</code>会被人绕过修改密码。</p><p>Config 文件可以帮助我们设定好需要的 API 提供商和需要哪些模型，我们下一个小节来说。</p><h3 id="配置-API-服务商和模型服务"><a href="#配置-API-服务商和模型服务" class="headerlink" title="配置 API 服务商和模型服务"></a>配置 API 服务商和模型服务</h3><p>配置例子：</p><figure class="highlight yaml"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">1.0</span><span class="number">.8</span></span><br><span class="line"></span><br><span class="line"><span class="attr">cache:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">interface:</span></span><br><span class="line">  <span class="comment"># Privacy policy settings</span></span><br><span class="line">  <span class="attr">privacyPolicy:</span></span><br><span class="line">    <span class="attr">externalUrl:</span> <span class="string">&quot;https://librechat.ai/privacy-policy&quot;</span></span><br><span class="line">    <span class="attr">openNewTab:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># Terms of service</span></span><br><span class="line">  <span class="attr">termsOfService:</span></span><br><span class="line">    <span class="attr">externalUrl:</span> <span class="string">&quot;https://librechat.ai/tos&quot;</span></span><br><span class="line">    <span class="attr">openNewTab:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">endpoints:</span></span><br><span class="line">  <span class="attr">custom:</span></span><br><span class="line">    <span class="comment"># Anyscale</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;Anyscale&quot;</span></span><br><span class="line">      <span class="attr">apiKey:</span> <span class="string">&quot;$&#123;ANYSCALE_API_KEY&#125;&quot;</span></span><br><span class="line">      <span class="attr">baseURL:</span> <span class="string">&quot;https://api.endpoints.anyscale.com/v1&quot;</span></span><br><span class="line">      <span class="attr">models:</span></span><br><span class="line">        <span class="attr">default:</span> [<span class="string">&quot;meta-llama/Llama-2-7b-chat-hf&quot;</span>]</span><br><span class="line">        <span class="attr">fetch:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleConvo:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleModel:</span> <span class="string">&quot;meta-llama/Llama-2-7b-chat-hf&quot;</span></span><br><span class="line">      <span class="attr">summarize:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">summaryModel:</span> <span class="string">&quot;meta-llama/Llama-2-7b-chat-hf&quot;</span></span><br><span class="line">      <span class="attr">forcePrompt:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">modelDisplayLabel:</span> <span class="string">&quot;Anyscale&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># APIpie</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;APIpie&quot;</span></span><br><span class="line">      <span class="attr">apiKey:</span> <span class="string">&quot;$&#123;APIPIE_API_KEY&#125;&quot;</span></span><br><span class="line">      <span class="attr">baseURL:</span> <span class="string">&quot;https://apipie.ai/v1/&quot;</span></span><br><span class="line">      <span class="attr">models:</span></span><br><span class="line">        <span class="attr">default:</span></span><br><span class="line">          [</span><br><span class="line">            <span class="string">&quot;gpt-4&quot;</span>,</span><br><span class="line">            <span class="string">&quot;gpt-4-turbo&quot;</span>,</span><br><span class="line">            <span class="string">&quot;gpt-3.5-turbo&quot;</span>,</span><br><span class="line">            <span class="string">&quot;claude-3-opus&quot;</span>,</span><br><span class="line">            <span class="string">&quot;claude-3-sonnet&quot;</span>,</span><br><span class="line">            <span class="string">&quot;claude-3-haiku&quot;</span>,</span><br><span class="line">            <span class="string">&quot;llama-3-70b-instruct&quot;</span>,</span><br><span class="line">            <span class="string">&quot;llama-3-8b-instruct&quot;</span>,</span><br><span class="line">            <span class="string">&quot;gemini-pro-1.5&quot;</span>,</span><br><span class="line">            <span class="string">&quot;gemini-pro&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mistral-large&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mistral-medium&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mistral-small&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mistral-tiny&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mixtral-8x22b&quot;</span>,</span><br><span class="line">          ]</span><br><span class="line">        <span class="attr">fetch:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">titleConvo:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleModel:</span> <span class="string">&quot;gpt-3.5-turbo&quot;</span></span><br><span class="line">      <span class="attr">dropParams:</span> [<span class="string">&quot;stream&quot;</span>]</span><br><span class="line"></span><br><span class="line">    <span class="comment">#cohere</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;cohere&quot;</span></span><br><span class="line">      <span class="attr">apiKey:</span> <span class="string">&quot;$&#123;COHERE_API_KEY&#125;&quot;</span></span><br><span class="line">      <span class="attr">baseURL:</span> <span class="string">&quot;https://api.cohere.ai/v1&quot;</span></span><br><span class="line">      <span class="attr">models:</span></span><br><span class="line">        <span class="attr">default:</span></span><br><span class="line">          [</span><br><span class="line">            <span class="string">&quot;command-r&quot;</span>,</span><br><span class="line">            <span class="string">&quot;command-r-plus&quot;</span>,</span><br><span class="line">            <span class="string">&quot;command-light&quot;</span>,</span><br><span class="line">            <span class="string">&quot;command-light-nightly&quot;</span>,</span><br><span class="line">            <span class="string">&quot;command&quot;</span>,</span><br><span class="line">            <span class="string">&quot;command-nightly&quot;</span>,</span><br><span class="line">          ]</span><br><span class="line">        <span class="attr">fetch:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">modelDisplayLabel:</span> <span class="string">&quot;cohere&quot;</span></span><br><span class="line">      <span class="attr">titleModel:</span> <span class="string">&quot;command&quot;</span></span><br><span class="line">      <span class="attr">dropParams:</span></span><br><span class="line">        [</span><br><span class="line">          <span class="string">&quot;stop&quot;</span>,</span><br><span class="line">          <span class="string">&quot;user&quot;</span>,</span><br><span class="line">          <span class="string">&quot;frequency_penalty&quot;</span>,</span><br><span class="line">          <span class="string">&quot;presence_penalty&quot;</span>,</span><br><span class="line">          <span class="string">&quot;temperature&quot;</span>,</span><br><span class="line">          <span class="string">&quot;top_p&quot;</span>,</span><br><span class="line">        ]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Fireworks</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;Fireworks&quot;</span></span><br><span class="line">      <span class="attr">apiKey:</span> <span class="string">&quot;$&#123;FIREWORKS_API_KEY&#125;&quot;</span></span><br><span class="line">      <span class="attr">baseURL:</span> <span class="string">&quot;https://api.fireworks.ai/inference/v1&quot;</span></span><br><span class="line">      <span class="attr">models:</span></span><br><span class="line">        <span class="attr">default:</span> [<span class="string">&quot;accounts/fireworks/models/mixtral-8x7b-instruct&quot;</span>]</span><br><span class="line">        <span class="attr">fetch:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleConvo:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleModel:</span> <span class="string">&quot;accounts/fireworks/models/llama-v2-7b-chat&quot;</span></span><br><span class="line">      <span class="attr">summarize:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">summaryModel:</span> <span class="string">&quot;accounts/fireworks/models/llama-v2-7b-chat&quot;</span></span><br><span class="line">      <span class="attr">forcePrompt:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">modelDisplayLabel:</span> <span class="string">&quot;Fireworks&quot;</span></span><br><span class="line">      <span class="attr">dropParams:</span> [<span class="string">&quot;user&quot;</span>]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># groq</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;groq&quot;</span></span><br><span class="line">      <span class="attr">apiKey:</span> <span class="string">&quot;$&#123;GROQ_API_KEY&#125;&quot;</span></span><br><span class="line">      <span class="attr">baseURL:</span> <span class="string">&quot;https://api.groq.com/openai/v1/&quot;</span></span><br><span class="line">      <span class="attr">models:</span></span><br><span class="line">        <span class="attr">default:</span></span><br><span class="line">          [</span><br><span class="line">            <span class="string">&quot;llama2-70b-4096&quot;</span>,</span><br><span class="line">            <span class="string">&quot;llama3-70b-8192&quot;</span>,</span><br><span class="line">            <span class="string">&quot;llama3-8b-8192&quot;</span>,</span><br><span class="line">            <span class="string">&quot;mixtral-8x7b-32768&quot;</span>,</span><br><span class="line">            <span class="string">&quot;gemma-7b-it&quot;</span>,</span><br><span class="line">          ]</span><br><span class="line">        <span class="attr">fetch:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">titleConvo:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">titleModel:</span> <span class="string">&quot;mixtral-8x7b-32768&quot;</span></span><br><span class="line">      <span class="attr">modelDisplayLabel:</span> <span class="string">&quot;groq&quot;</span></span><br></pre></td></tr></table></figure><p>模型的提供商主要在<code>endpoints</code>里面提供，LibreChat 支持大量的模型供应商，例如 OpenAI, Google, XAI, Openrouter 等等，这些厂商一般都是通过 OpenAI 兼容的端点来实现的，所以我们只需要按照例子中把<code>name</code>修改为我们自己的供应商名称，然后修改<code>apiKey</code> 和 <code>baseURL</code> 即可。</p><div class="tag-plugin colorful note" color="green"><div class="title">你可以把apiKey</div><div class="body"><p>设置为“user_provided”，这允许你在网页端手动设置并保存在你的浏览器内，防止泄露</p></div></div><p>然后<code>models</code>填写你需要对话的模型 ID，这个一般模型供应商都会在后台提供。想要设置更多的参数，请参考<a href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/custom_endpoint">官方文档</a>。</p><p>最后，把你写好的配置文件放到任意的仓库中，复制这个文件的链接，填入<code>CONFIG_PATH</code>即可。</p><h3 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h3><p>填写完 <code>secrets</code> 和 <code>variables</code> 后，单击窗口底部的 <code>Duplicate Space</code></p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/9023e7766a79fa5db091249da9cfa066.png" alt="image"></p><p>项目现在将构建，这将需要几分钟</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/70d3436d4c858487ed2f105deb910feb.png" alt="image"></p><p>准备就绪后， <code>Building</code> 将变为 <code>Running</code> 状态</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/b89709879cd34c7e7b1ee7d70c1b992c.png" alt="image"></p><p>你将能够访问 LibreChat！</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/30/f59078214e9890ea4b541baa209e54e9.png" alt="image"></p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>您现在可以通过当前 URL 访问它。如果您不想使用 Hugging Face 访问它，可以点击右上角的三个点，选择<code>Embed this Space</code>, 系统将会弹出一个模态框，里面有个<code>src</code>的一串链接，这个链接就是你部署好的地址，通过这个地址，你就可以脱离 hugging face 直接访问了。通常，这个链接类似于</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://username-projectname.hf.space/</span><br></pre></td></tr></table></figure><p>🎉 恭喜，您已成功在 Hugging Face 上部署 LibreChat！🤗</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;如今 AI 大语言模型内卷严重，各种大语言模型层出不穷，今天 gpt 的生图模型很强大，明天 Gemini-pro-2.5 又突破天际，后天 Claude 4.0 又横空出世。用户可能需要不断的去开通各个大模型的服务，极度浪费金钱。那么最简单的方法或许就是使用 API 服务商提供的服务了，一个 API 可以用到所有的模型，而且按量付费，不用不花钱。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
  </entry>
  
  <entry>
    <title>Vercel搭建Hexo后台Qexo的方法</title>
    <link href="https://blog.xiaohanys.top/vercel-based-qexo/"/>
    <id>https://blog.xiaohanys.top/vercel-based-qexo/</id>
    <published>2025-05-28T21:46:33.521Z</published>
    <updated>2026-03-26T09:21:36.505Z</updated>
    
    <content type="html"><![CDATA[<p>鉴于 Qexo 官方文档内容非常简略（简陋），在尝试无数次失败（主要是网上的教程都有很多问题）后，将成功搭建的流程和方法记录在本文中。</p><p>在开始之前，我需要简单介绍一下我们云端 Hexo 后台的搭建原理：就是首先你得把所有博客的源代码（<strong>是 Hexo 源码，不是生成后的静态博客源码</strong>）托管到 Github 仓库中，然后通过设置一系列的 Key, 允许我们搭建在 Vercel 的 <strong>Qexo 服务可以交互的编辑和提交内容到 Github 仓库</strong>，这样我们在云端编辑的博文</p><span id="more"></span><p>就可以自动的上传到 Github 仓库了，<strong>所以这个 Github 仓库一定要是私有</strong>的，否则你的源码就泄露了。然后我们添加一个 Github Action, 当在云端编辑好了后，我们用 <strong>Action 来编译出静态博客来推送到 Github Pages</strong>. 然后我们可以在 Vercel 或者 CloudFlare 关联我们的 Github Pages，这样相比 Pages 会有更好的访问速度和 SEO。</p><h1 id="部署-Qexo-环境"><a href="#部署-Qexo-环境" class="headerlink" title="部署 Qexo 环境"></a>部署 Qexo 环境</h1><p>官方提供了四种方式来部署 Qexo 环境，其中一种允许你在本地进行部署，另外三种各自使用了不同网站提供的免费数据库服务。综合考虑操作便捷性和成功率，这里选用 Vercel 提供的免费 PostgreSQL 服务进行部署。</p><p>首先点击<a href="https://github.com/am-abudu/Qexo">这里</a>进入 Qexo 的仓库克界面，我们点击右上角的 Fork 按钮将仓库克隆到我们自己的仓库里，然后打开 <a href="https://vercel.com/">Vercel</a>登录你得 Github 账号，并且使用刚才克隆得仓库来部署一个服务，这样可以让我们及时得同步上游版本，从而更好的更新 Qexo 系统版本。</p><p>设置好仓库名称后，点击 <code>Create</code> 创建，下边会有一个 <code>Deploy</code> 界面。Vercel 在创建和更改仓库时会自动进行一次部署，因此创建完毕后部署将会自动启动，并且这第一次部署是一定会失败的。因为 Qexo 所依赖的数据库还没有配置。因此点击网页左上角的三角形符号，不出意外的话，界面将是这个样子：</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/29/c692309b99f17ea0e03cc59325b4347d.png">(我这里是已经配置好了 Qexo)</p><p>然后我们开始配置 PostgreSQL 数据库，在 <code>Storage</code> 界面可以申请，点击右上角 <code>Create Database</code>并选择 <code>Neon</code> 或者 <code>Supabase</code>，Vercel 使用外部服务提供 PostgreSQL 服务，如果你先前没有配置过——点击 <code>Continue</code>进入数据库连接配置，在 <code>Connect</code>界面选择地区为 <code>Washington DC</code>或者 <code>USA (east)</code>。创建完毕后，在 <code>Storage</code>选项卡里选择进入你创建的数据库配置界面。在左侧边栏点击 <code>Project</code>，接着点击 <code>Connect Project</code>：</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/29/f490c2f579fd6715345592f60b6f2c53.png" alt="image"></p><p>在上面 <code>Search Projects...</code>选择自己想要部署 Qexo 的仓库即可，Vercel 这里的好处就是它可以全自动的执行前端和数据库的链接，可以保证完全正确的链接，这里我们回到项目 build 界面，点击 <code>Build Logs</code>按钮，点击 <code>Redeploy</code>按钮即可以成功部署了。</p><div class="tag-plugin colorful note" color="blue"><div class="title">这里需要注意的是，因为</div><div class="body"><p>Vercel在国内是无法访问的，但是添加一个自定义的域名，然后基于该域名提供的 DNS 服务就可以正确打开了，所以推荐你设置一下自定义域名。添加域名的操作就很简单了，直接点击Settings-&gt;Domain就可以了，这里就不再赘述了。</p></div></div><h1 id="初始化-Qexo"><a href="#初始化-Qexo" class="headerlink" title="初始化 Qexo"></a>初始化 Qexo</h1><h2 id="Github-配置"><a href="#Github-配置" class="headerlink" title="Github 配置"></a>Github 配置</h2><p>部署完毕后，切换到绑定的域名，本例中我们转到 <code>https://admin.xiaohanys.xyz</code>。首先需要你填写用户名和密码，Token 留空即可。接下来设置 Github 链接，如果你使用 Hexo，并在 Github 上托管，在 Github 的配置界面，你会看到这几项：<br>‍‍‍‍‍‌‌‌‍<img src="https://tncache1-f1.v3mh.com/image/2025/05/29/9c4e2249835de24d20437d0ce00155c0.png" alt="image"></p><p>Github 密钥这一项，你需要在<a href="https://github.com/settings/tokens">Github 设置</a>中申请。右上角选择 <code>Generate New Token</code>，有两个选项，选择 <code>classic</code>。接着完成身份验证。改变如下几项：</p><p><img src="https://tncache1-f1.v3mh.com/image/2025/05/29/40448fb24c06861e4f029a03416ea761.png"></p><p><code>Note</code> 必填，作为这个 token 的使用目的；<code>Expiration</code> 是生效期限，安全起见建议设置一个较短的期限，然后定时重置，重新配置 Qexo 设置，这里我选择的是永久有效；在下边的生效条目里，保证 <code>repo</code> 下的复选框全部勾选，建议同时勾选 <code>workflow</code>，但官方不建议给出所有权限。这么做的目的是保证 Qexo 有足够权限访问 Github API 从而在线修改 Github 博客源码的内容。</p><p>申请完毕后复制下来，出于安全，Github 仅在 token 初次创建完毕后给出复制选项，所以尽快保存，并填入初始化界面的“Github 密钥”文本框中。</p><p>然后在 Github 里新建仓库，用于存放博客源码。我建议各位使用 Github 官方的客户端来执行仓库推送的过程，用命令难免会出错，出错还不方便 Debug。且如果是 Windows 电脑不用安装 Git bash 命令了。Github 的官方客户端从此<a href="https://github.com/apps/desktop">下载</a>。</p><div class="tag-plugin colorful note" color="blue"><div class="title">使用</div><div class="body"><p>Github Desktop 要做的就是把我们的所有 hexo 的源码推送到一个私有的仓库里， 刚才设置界面的“Github 仓库”这一项就填刚刚创建并上传的源码仓库，格式是 <code>&lt;username&gt;/&lt;repo&gt;</code>（例：<code>XXX/xiaohanys</code>）。 “项目分支”填源代码仓库的主要分支，一般是 <code>master</code>；“博客路径”留空即可。</p></div></div><h2 id="Vercel-配置"><a href="#Vercel-配置" class="headerlink" title="Vercel 配置"></a>Vercel 配置</h2><p>设置好 Github 的链接后就是需要我们设置 Vercel 的密钥，“VERCEL_TOKEN”一项，需要在<a href="https://vercel.com/account/tokens">这里</a>生成。</p><p>同样是填写 token 名称、生效范围（这里选择 <code>xxx&#39;s projetcs</code>）和生效期限（建议期限短些）。完毕后点击 <code>Create</code> 生成密钥，也是需要尽快复制下来，粘贴到“VERCEL_TOKEN”里。</p><p>“PROJECT_ID”则需要回到 Vercel 对应的项目的 <code>Settings</code> 里，在 <code>General</code> 选项卡中向下翻到 <code>Project ID</code> 并复制内容，粘贴到 <code>PROJECT_ID</code> 中就完成 Vercel 配置了。</p><p>设置完毕后就可以从 <code>https://admin.xiaohanys.xyz</code> 快捷进入管理界面了。</p><h2 id="Github-Action"><a href="#Github-Action" class="headerlink" title="Github Action"></a>Github Action</h2><p>在我们本地的博客源码根目录（就是你执行 <code>hexo clean &amp; hexo g &amp; hexo d</code> 的文件夹）创建一个文件，路径为 <code>.github/workflows/main.yml</code>，然后我们将下面的代码复制进来：</p><figure class="highlight yaml"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">auto</span> <span class="string">deploy</span> <span class="string">Hexo</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span> <span class="string">workflow_dispatch</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">checkout</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">submodules:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">install</span> <span class="string">Pandoc</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">nikeee/setup-pandoc@v1</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">setup</span> <span class="string">Node.js</span> <span class="number">23.10</span><span class="number">.0</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/setup-node@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;23.10.0&quot;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">install</span> <span class="string">Hexo</span> <span class="string">CI</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          export TS=&#x27;Asia/Shanghai&#x27;</span></span><br><span class="line"><span class="string">          npm install hexo-cli -g</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">cache</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/cache@v4</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">cache-dependencies</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">node_modules</span></span><br><span class="line">          <span class="attr">key:</span> <span class="string">$&#123;&#123;runner.OS&#125;&#125;-$&#123;&#123;hashFiles(&#x27;**/package-lock.json&#x27;)&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">install</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          # Install Plugins with &#x27;npm install&#x27;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">install</span> <span class="string">plugins</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">steps.cache-dependencies.outputs.cache-hit</span> <span class="type">!=</span> <span class="string">&#x27;true&#x27;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          npm install</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">static</span> <span class="string">pages</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">hexo</span> <span class="string">generate</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">page</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">personal_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">publish_dir:</span> <span class="string">./public</span></span><br><span class="line">          <span class="attr">external_repository:</span> <span class="string">username/username.github.io</span> <span class="comment">#usename改成你的github用户名</span></span><br><span class="line">          <span class="attr">publish_branch:</span> <span class="string">master</span></span><br></pre></td></tr></table></figure><p>这里我们使用手动触发 <code>workflow_dispatch</code>,是因为 Qexo 改动一个文件就会 push 一次代码，比如我们只是创建了一个新的博客文件，还没有写内容，这样就会自动构建一次，非常消耗资源，这也是我觉得需要改进的地方。目前我们只有手动控制才能更加精准。</p><p>最后一步将生成的博客内容部署到 Pages，如果你打算直接部署到当前仓库的 gh-pages 分支，那么可以不需要添加 <code>external_repository</code>, 你只需要 <code>github_token</code>和 <code>publish_dir</code> 即可。例如这样：</p><figure class="highlight yaml"><table><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">github_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">publish_dir:</span> <span class="string">./public</span></span><br></pre></td></tr></table></figure><p>但是如果你打算把博客文章部署到其他的仓库，那么你需要把 <code>github_token</code>改成 <code>personal_token</code>。你可以在<a href="https://github.com/settings/personal-access-tokens">此处</a>申请,并尽可能的开通全部读和写的权限，防止失败。</p><h1 id="博客生成流程"><a href="#博客生成流程" class="headerlink" title="博客生成流程"></a>博客生成流程</h1><p>最后，打开你得 Hexo 云端后台，点击文章，新建文章，然后随意写一篇博客，推送到 Github, 然后手动开启 Github Action, 你就可以在你得博客地址看到你得博客上线了。</p><p>如果你需要更多的内容，请查询以下的网站：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://www.oplog.cn/qexo/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://www.oplog.cn/qexo/</span><span class="cap link footnote">https://www.oplog.cn/qexo/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://justpureh2o.cn/articles/17536/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://justpureh2o.cn/articles/17536/</span><span class="cap link footnote">https://justpureh2o.cn/articles/17536/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://github.com/peaceiris/actions-gh-pages" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://github.com/peaceiris/actions-gh-pages</span><span class="cap link footnote">https://github.com/peaceiris/actions-gh-pages</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;鉴于 Qexo 官方文档内容非常简略（简陋），在尝试无数次失败（主要是网上的教程都有很多问题）后，将成功搭建的流程和方法记录在本文中。&lt;/p&gt;
&lt;p&gt;在开始之前，我需要简单介绍一下我们云端 Hexo 后台的搭建原理：就是首先你得把所有博客的源代码（&lt;strong&gt;是 Hexo 源码，不是生成后的静态博客源码&lt;/strong&gt;）托管到 Github 仓库中，然后通过设置一系列的 Key, 允许我们搭建在 Vercel 的 &lt;strong&gt;Qexo 服务可以交互的编辑和提交内容到 Github 仓库&lt;/strong&gt;，这样我们在云端编辑的博文&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
  </entry>
  
  <entry>
    <title>用Shiny 打造一个简易但功能完善的AI 绘画APP</title>
    <link href="https://blog.xiaohanys.top/Py-shiny-AIpaint/"/>
    <id>https://blog.xiaohanys.top/Py-shiny-AIpaint/</id>
    <published>2025-03-14T02:43:11.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>众所周知，Shiny 是 Rstudio 开发的一款基于 R 语言的网页应用程序，允许用户将 R 功能脚本编写成简易的 UI 界面，从而允许不懂编程的人使用，但是 R 语言的语法和功能较为复杂，且执行效率较低，对并发的支持也不好。幸好，Rstudio 同时开发了基于 Python 的 Shiny 程序，且最近新推出的 Express 功能让代码更加的简洁，本文将介绍如何使用 Shiny Express 开发一个简易但功能完善的 AI 绘画 APP。</p><span id="more"></span><h3 id="如何安装-Shiny-Express"><a href="#如何安装-Shiny-Express" class="headerlink" title="如何安装 Shiny Express"></a>如何安装 Shiny Express</h3><p>大家可以访问<a href="https://shiny.posit.co/py/docs/install-create-run.html">官方网站</a>进行了解，简单来说我们简易构建一个纯净的虚拟环境，然后安装对应的包即可，例如：</p><figure class="highlight bash"><table><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"><span class="built_in">mkdir</span> myapp</span><br><span class="line"><span class="built_in">cd</span> myapp</span><br><span class="line"><span class="comment"># Create a virtual environment in the .venv subdirectory</span></span><br><span class="line">python3 -m venv .venv</span><br><span class="line"><span class="comment"># Activate the virtual environment</span></span><br><span class="line"><span class="built_in">source</span> .venv/bin/activate</span><br></pre></td></tr></table></figure><p>然后在虚拟环境下安装 Shiny Express：</p><figure class="highlight bash"><table><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">pip install shiny</span><br><span class="line"></span><br><span class="line"><span class="comment">#或者安装开发版本</span></span><br><span class="line">pip install git+https://github.com/posit-dev/py-htmltools.git</span><br><span class="line">pip install git+https://github.com/posit-dev/py-shiny.git</span><br></pre></td></tr></table></figure><h3 id="AI-绘画-API"><a href="#AI-绘画-API" class="headerlink" title="AI 绘画 API"></a>AI 绘画 API</h3><p>我们的 AI 绘画 API 还是来自 CloudFlare 的 flux 模型，具体参照前文：<br><a href="https://xiaohanys.top/py-cf-flux/">如何使用 Python 调用 CloudFlare WorkerAI 提供的 Flux 模型</a></p><h3 id="代码结构"><a href="#代码结构" class="headerlink" title="代码结构"></a>代码结构</h3><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">- myapp  ### 这里是主目录</span><br><span class="line">  - app.py  ### 这个是主程序</span><br><span class="line">  - utils.py  ### 这里写调取API的功能</span><br><span class="line">  - .venv   ### 虚拟环境</span><br></pre></td></tr></table></figure><h3 id="调取-API-的功能的实现-utils-py"><a href="#调取-API-的功能的实现-utils-py" class="headerlink" title="调取 API 的功能的实现(utils.py)"></a>调取 API 的功能的实现(utils.py)</h3><p>我们要实现以下功能：</p><ol><li>调用 API 获取图片</li><li>图片保存到本地</li><li>使用大语言模型对提示词翻译并优化</li><li>全程使用异步函数</li></ol><p>具体的代码如下：</p><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> base64</span><br><span class="line"><span class="keyword">from</span> openai <span class="keyword">import</span> AsyncOpenAI</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"><span class="keyword">import</span> tempfile</span><br><span class="line"><span class="keyword">import</span> aiohttp</span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">WorkerImage</span>():</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, account_id, api_token</span>):</span><br><span class="line">        <span class="variable language_">self</span>.flux_system_prompt = <span class="string">&quot;...提示词前文有提到，这里不重复写了&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.account_id = account_id</span><br><span class="line">        <span class="variable language_">self</span>.api_token = api_token</span><br><span class="line">        <span class="variable language_">self</span>.endpoint = (</span><br><span class="line">            <span class="string">f&quot;https://api.cloudflare.com/client/v4/accounts/<span class="subst">&#123;account_id&#125;</span>/ai/run/@cf/black-forest-labs/flux-1-schnell&quot;</span></span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">self</span>.headers = &#123;</span><br><span class="line">            <span class="string">&quot;Authorization&quot;</span>: <span class="string">f&quot;Bearer <span class="subst">&#123;api_token&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;Content-Type&quot;</span>: <span class="string">&quot;application/json&quot;</span>,</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">_get_flux_image_data</span>(<span class="params">self, prompt, num_steps</span>):</span><br><span class="line">        payload = &#123;</span><br><span class="line">            <span class="string">&quot;prompt&quot;</span>: prompt,</span><br><span class="line">            <span class="string">&quot;num_steps&quot;</span>: num_steps</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">async</span> <span class="keyword">with</span> aiohttp.ClientSession() <span class="keyword">as</span> session:</span><br><span class="line">            <span class="keyword">async</span> <span class="keyword">with</span> session.post(<span class="variable language_">self</span>.endpoint, headers=<span class="variable language_">self</span>.headers, json=payload) <span class="keyword">as</span> response:</span><br><span class="line">                <span class="keyword">if</span> response.status == <span class="number">400</span>:</span><br><span class="line">                    <span class="comment"># raise error</span></span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    result = <span class="keyword">await</span> response.json()</span><br><span class="line">                    image_data = base64.b64decode(result[<span class="string">&#x27;result&#x27;</span>][<span class="string">&#x27;image&#x27;</span>])</span><br><span class="line">                    <span class="keyword">return</span> image_data</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_save_image_to_tmp_file</span>(<span class="params">self, image_data</span>):</span><br><span class="line">        <span class="built_in">dir</span> = Path(__file__).resolve().parent</span><br><span class="line">        <span class="keyword">with</span> tempfile.NamedTemporaryFile(<span class="built_in">dir</span>=<span class="built_in">dir</span>, suffix=<span class="string">&#x27;.png&#x27;</span>, delete=<span class="literal">False</span>) <span class="keyword">as</span> f:</span><br><span class="line">            f.write(image_data)</span><br><span class="line">            temp_file_path = f.name</span><br><span class="line">        <span class="keyword">return</span> temp_file_path</span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">get_translate_promt</span>(<span class="params">self, prompt</span>):</span><br><span class="line"></span><br><span class="line">        client = AsyncOpenAI(</span><br><span class="line">            <span class="comment"># This is the default and can be omitted</span></span><br><span class="line">            api_key=<span class="variable language_">self</span>.api_token,</span><br><span class="line">            base_url=<span class="string">f&quot;https://api.cloudflare.com/client/v4/accounts/<span class="subst">&#123;self.account_id&#125;</span>/ai/v1&quot;</span>,</span><br><span class="line">            max_retries=<span class="number">5</span>,</span><br><span class="line">        )</span><br><span class="line">        chat_completion = <span class="keyword">await</span> client.chat.completions.create(</span><br><span class="line">            messages=[</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="string">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;content&quot;</span>: <span class="variable language_">self</span>.flux_system_prompt,</span><br><span class="line">                &#125;,</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;content&quot;</span>: prompt,</span><br><span class="line">                &#125;</span><br><span class="line">            ],</span><br><span class="line">            model=<span class="string">&quot;@cf/meta/llama-3.3-70b-instruct-fp8-fast&quot;</span>,</span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">return</span> chat_completion.choices[<span class="number">0</span>].message.content</span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">get_image</span>(<span class="params">self, prompt, num_steps, is_translate=<span class="literal">False</span></span>):</span><br><span class="line">        new_prompts = <span class="string">&quot;&quot;</span></span><br><span class="line">        <span class="keyword">if</span> is_translate:</span><br><span class="line">            new_prompts = <span class="keyword">await</span> <span class="variable language_">self</span>.get_translate_promt(prompt)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            new_prompts = prompt</span><br><span class="line">        image_data = <span class="keyword">await</span> <span class="variable language_">self</span>._get_flux_image_data(new_prompts, num_steps)</span><br><span class="line">        <span class="keyword">if</span> image_data:</span><br><span class="line">            local_file = <span class="variable language_">self</span>._save_image_to_tmp_file(image_data)</span><br><span class="line">            local_file = local_file.split(<span class="string">&quot;/&quot;</span>)[-<span class="number">1</span>]</span><br><span class="line">            <span class="keyword">return</span> local_file, new_prompts</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;image data is None,please check your inputs&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span>, new_prompts</span><br></pre></td></tr></table></figure><p>这个类需要使用 account_id 和 API, 需要去 Cloudflare 获取！</p><h3 id="主程序的实现-app-py"><a href="#主程序的实现-app-py" class="headerlink" title="主程序的实现(app.py)"></a>主程序的实现(app.py)</h3><p>我们要实现以下功能：</p><ol><li>一个 NavaBar, 为后续增加功能准备</li><li>一个 SideBar, 用来收集用户的输入，作为主要交互区域</li><li>一个 MainPanel, 用来显示用户的输出</li></ol><p>完成后的界面如图：</p><p><img src="https://i.postimg.cc/RFZ2sWN2/1741921723739.jpg"></p><p>具体的代码如下：</p><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> shiny.express <span class="keyword">import</span> ui, render, <span class="built_in">input</span></span><br><span class="line"><span class="keyword">from</span> shiny <span class="keyword">import</span> reactive</span><br><span class="line"><span class="keyword">from</span> utils <span class="keyword">import</span> WorkerImage</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line">ui.page_opts(title=<span class="string">&quot;Image Generation&quot;</span>, fillable=<span class="literal">True</span>, <span class="built_in">id</span>=<span class="string">&quot;page&quot;</span>)</span><br><span class="line"></span><br><span class="line">prompts = reactive.value(<span class="string">&quot;a dog&quot;</span>)</span><br><span class="line">generated_image = reactive.value(<span class="literal">None</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@reactive.effect</span></span><br><span class="line"><span class="meta">@reactive.event(<span class="params"><span class="built_in">input</span>.submit</span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">generate_image</span>():</span><br><span class="line">    account_id = <span class="string">&quot;XXXXXXXXXXXXXXXXXXXXX&quot;</span></span><br><span class="line">    api_key = <span class="string">&quot;XXXXXXXXXXXXXXXXXXXXXXX&quot;</span></span><br><span class="line">    worker = WorkerImage(account_id, api_key)</span><br><span class="line">    <span class="built_in">dir</span> = Path(__file__).resolve().parent</span><br><span class="line">    final_link, new_prompts = <span class="keyword">await</span> worker.get_image(<span class="built_in">input</span>.prompt(), num_steps=<span class="built_in">input</span>.steps(), is_translate=<span class="built_in">input</span>.optim())</span><br><span class="line">    prompts.<span class="built_in">set</span>(new_prompts)</span><br><span class="line">    <span class="keyword">if</span> final_link <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line">        generated_image.<span class="built_in">set</span>(&#123;<span class="string">&quot;src&quot;</span>: <span class="built_in">str</span>(<span class="built_in">dir</span> / final_link), <span class="string">&quot;width&quot;</span>: <span class="string">&quot;500px&quot;</span>&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> ui.sidebar(width=<span class="number">450</span>):</span><br><span class="line">    <span class="keyword">with</span> ui.tooltip(placement=<span class="string">&quot;right&quot;</span>):</span><br><span class="line">        ui.input_text_area(<span class="string">&quot;prompt&quot;</span>, <span class="string">&quot;Prompt&quot;</span>, placeholder=<span class="string">&quot;Enter your prompt here&quot;</span>, rows=<span class="number">5</span>)</span><br><span class="line">        <span class="string">&quot;You can use English or Chinese to generate images.&quot;</span></span><br><span class="line">    ui.input_slider(<span class="string">&quot;steps&quot;</span>, <span class="string">&quot;Number of Steps:&quot;</span>, <span class="built_in">min</span>=<span class="number">1</span>, <span class="built_in">max</span>=<span class="number">8</span>, value=<span class="number">4</span>)</span><br><span class="line">    <span class="keyword">with</span> ui.layout_columns(class_=<span class="string">&quot;d-flex align-items-center&quot;</span>):</span><br><span class="line">        <span class="keyword">with</span> ui.tooltip(placement=<span class="string">&quot;bottom&quot;</span>):</span><br><span class="line">            ui.input_switch(<span class="string">&quot;optim&quot;</span>, <span class="string">&quot;Imagine&quot;</span>, <span class="literal">False</span>)</span><br><span class="line">            <span class="string">&quot;Switch on to optimize your prompt.&quot;</span></span><br><span class="line">        ui.input_action_button(<span class="string">&quot;submit&quot;</span>, <span class="string">&quot;Generate&quot;</span>,class_=<span class="string">&quot;btn-primary&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> ui.nav_panel(<span class="string">&quot;AI Painting&quot;</span>):</span><br><span class="line">    <span class="keyword">with</span> ui.card():</span><br><span class="line">        ui.card_header(<span class="string">&quot;Generated Images&quot;</span>)</span><br><span class="line"><span class="meta">        @render.image(<span class="params">delete_file=<span class="literal">True</span></span>)</span></span><br><span class="line">        <span class="keyword">def</span> <span class="title function_">image</span>():</span><br><span class="line">            <span class="keyword">return</span> generated_image() <span class="keyword">if</span> generated_image() <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line">        <span class="keyword">with</span> ui.card_footer(<span class="string">&quot;Optimized Prompts:&quot;</span>):</span><br><span class="line"><span class="meta">            @render.text</span></span><br><span class="line">            <span class="keyword">def</span> <span class="title function_">get_prompts</span>():</span><br><span class="line">                <span class="keyword">return</span> prompts()</span><br></pre></td></tr></table></figure><p>是的，整个代码非常简洁，我们只用了很少的代码就完成了相当复杂的功能，值得称赞！</p><h3 id="网站演示"><a href="#网站演示" class="headerlink" title="网站演示"></a>网站演示</h3><p>可以访问以下链接访问演示网站：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://dvbuml-guangguang-pan.shinyapps.io/imagegeneration/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://dvbuml-guangguang-pan.shinyapps.io/imagegeneration/</span><span class="cap link footnote">https://dvbuml-guangguang-pan.shinyapps.io/imagegeneration/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;众所周知，Shiny 是 Rstudio 开发的一款基于 R 语言的网页应用程序，允许用户将 R 功能脚本编写成简易的 UI 界面，从而允许不懂编程的人使用，但是 R 语言的语法和功能较为复杂，且执行效率较低，对并发的支持也不好。幸好，Rstudio 同时开发了基于 Python 的 Shiny 程序，且最近新推出的 Express 功能让代码更加的简洁，本文将介绍如何使用 Shiny Express 开发一个简易但功能完善的 AI 绘画 APP。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>免费创建Flux生图API</title>
    <link href="https://blog.xiaohanys.top/create-ai-image-api/"/>
    <id>https://blog.xiaohanys.top/create-ai-image-api/</id>
    <published>2024-11-20T09:11:44.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>众所周知，Flux 是一个 120 亿参数的大模型，普通人本地根本跑不起来，那么今天我来教大家免费白嫖一个无服务器容器类的网站，帮我们一键化构建一个生图 API, 每个人每个月有免费的 30 刀的赠费，可以使用 H100, A100 等超强显卡资源。</p><span id="more"></span><h2 id="注册-Models-网站"><a href="#注册-Models-网站" class="headerlink" title="注册 Models 网站"></a>注册 Models 网站</h2><p>首先，打开这个网站<a href="https://modal.com/">models</a>，我们注册一下：<br><img src="https://i.postimg.cc/zDtTYR2N/2024-11-20-17-21-05.png"></p><p>然后进入 Dashboard 页面，在这里你可以管理你所有创建的 APP，你也可以在右上角查看你的可用余额，也就是 30 美元。</p><h2 id="创建-Token"><a href="#创建-Token" class="headerlink" title="创建 Token"></a>创建 Token</h2><p>你需要先安装脚手架，并且申请一个 token 才可以从本地创建 App 并上传，我们运行：</p><p>首先创建一个本地的虚拟环境；</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> Models &amp;&amp; <span class="built_in">cd</span> Models</span><br><span class="line">python -m venv .venv</span><br><span class="line"><span class="built_in">source</span> .venv/bin/activate</span><br></pre></td></tr></table></figure><p>然后安装包并申请 token</p><figure class="highlight python"><table><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">pip install modal</span><br><span class="line">python3 -m modal setup</span><br></pre></td></tr></table></figure><h2 id="一键化脚本"><a href="#一键化脚本" class="headerlink" title="一键化脚本"></a>一键化脚本</h2><p>我把一键化脚本放上来，另外，这个脚本使用了 Flux.1-dev 以及一个增加细节的 lora, 具体可以查看代码：</p><p>新建一个文件，你可以叫他<code>Flux_cli.py</code>;将下面的代码复制粘贴进去，并且按照下面的指示，填写两个地方。如果对 GPU 没有要求，只需要添加你的<code>hf-token</code>.</p><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> io</span><br><span class="line"><span class="keyword">import</span> random</span><br><span class="line"><span class="keyword">import</span> modal</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">MINUTES = <span class="number">60</span></span><br><span class="line"></span><br><span class="line">app = modal.App(<span class="string">&quot;Flux.1-dev&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># config</span></span><br><span class="line">image = (</span><br><span class="line">    modal.Image.debian_slim(python_version=<span class="string">&quot;3.12&quot;</span>)</span><br><span class="line">    .pip_install(</span><br><span class="line">        <span class="string">&quot;accelerate==0.33.0&quot;</span>,</span><br><span class="line">        <span class="string">&quot;diffusers==0.31.0&quot;</span>,</span><br><span class="line">        <span class="string">&quot;fastapi[standard]==0.115.4&quot;</span>,</span><br><span class="line">        <span class="string">&quot;huggingface-hub[hf_transfer]==0.25.2&quot;</span>,</span><br><span class="line">        <span class="string">&quot;sentencepiece==0.2.0&quot;</span>,</span><br><span class="line">        <span class="string">&quot;torch==2.5.1&quot;</span>,</span><br><span class="line">        <span class="string">&quot;torchvision==0.20.1&quot;</span>,</span><br><span class="line">        <span class="string">&quot;transformers~=4.44.0&quot;</span>,</span><br><span class="line">        <span class="string">&quot;peft&quot;</span></span><br><span class="line">    )</span><br><span class="line">    .env(&#123;<span class="string">&quot;HF_HUB_ENABLE_HF_TRANSFER&quot;</span>: <span class="string">&quot;1&quot;</span>&#125;)  <span class="comment"># faster downloads</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> image.imports():</span><br><span class="line">    <span class="keyword">import</span> diffusers</span><br><span class="line">    <span class="keyword">import</span> torch</span><br><span class="line">    <span class="keyword">from</span> fastapi <span class="keyword">import</span> Response</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">model_id = <span class="string">&quot;black-forest-labs/FLUX.1-dev&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># inference</span></span><br><span class="line"><span class="comment">#### 这里可以选择显卡，你可以选择H100</span></span><br><span class="line"><span class="meta">@app.cls(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="meta">    image=image,</span></span></span><br><span class="line"><span class="params"><span class="meta">    gpu=<span class="string">&quot;A100-40GB&quot;</span>,</span></span></span><br><span class="line"><span class="params"><span class="meta">    timeout=<span class="number">10</span> * MINUTES,</span></span></span><br><span class="line"><span class="params"><span class="meta"></span>)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Inference</span>:</span><br><span class="line"><span class="meta">    @modal.build()</span></span><br><span class="line"><span class="meta">    @modal.enter()</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">initialize</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="variable language_">self</span>.pipe = diffusers.FluxPipeline.from_pretrained(</span><br><span class="line">            model_id,</span><br><span class="line">            <span class="comment">### 注意这里你要去hugging face网站申请token,才能访问Flux大模型</span></span><br><span class="line">            token=<span class="string">&quot;&quot;</span>,</span><br><span class="line">            torch_dtype=torch.bfloat16,</span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">self</span>.pipe.load_lora_weights(<span class="string">&quot;Shakker-Labs/FLUX.1-dev-LoRA-add-details&quot;</span>, weight_name=<span class="string">&quot;FLUX-dev-lora-add_details.safetensors&quot;</span>)</span><br><span class="line">        <span class="variable language_">self</span>.pipe.fuse_lora(lora_scale=<span class="number">0.45</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">    @modal.enter()</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">move_to_gpu</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="variable language_">self</span>.pipe.to(<span class="string">&quot;cuda&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">    @modal.method()</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">run</span>(<span class="params"></span></span><br><span class="line"><span class="params">        self, prompt: <span class="built_in">str</span>, width: <span class="built_in">int</span> = <span class="number">768</span>, height: <span class="built_in">int</span> = <span class="number">1024</span>, steps: <span class="built_in">int</span> = <span class="number">28</span>, cfg: <span class="built_in">float</span> = <span class="number">4.5</span>, batch_size: <span class="built_in">int</span> = <span class="number">1</span>, seed: <span class="built_in">int</span> = <span class="literal">None</span></span></span><br><span class="line"><span class="params">    </span>) -&gt; <span class="built_in">list</span>[<span class="built_in">bytes</span>]:</span><br><span class="line">        seed = seed <span class="keyword">if</span> seed <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">else</span> random.randint(<span class="number">0</span>, <span class="number">2</span>**<span class="number">32</span> - <span class="number">1</span>)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;seeding RNG with&quot;</span>, seed)</span><br><span class="line">        torch.manual_seed(seed)</span><br><span class="line">        images = <span class="variable language_">self</span>.pipe(</span><br><span class="line">            prompt,</span><br><span class="line">            <span class="comment"># outputting multiple images per prompt is much cheaper than separate calls</span></span><br><span class="line">            num_images_per_prompt=batch_size,</span><br><span class="line">            num_inference_steps=steps,</span><br><span class="line">            height=height,</span><br><span class="line">            width=width,  <span class="comment"># turbo is tuned to run in four steps</span></span><br><span class="line">            guidance_scale=cfg,  <span class="comment"># turbo doesn&#x27;t use CFG</span></span><br><span class="line">            <span class="comment"># T5-XXL text encoder supports longer sequences, more complex prompts</span></span><br><span class="line">            max_sequence_length=<span class="number">512</span>,</span><br><span class="line">        ).images</span><br><span class="line"></span><br><span class="line">        image_output = []</span><br><span class="line">        <span class="keyword">for</span> image <span class="keyword">in</span> images:</span><br><span class="line">            <span class="keyword">with</span> io.BytesIO() <span class="keyword">as</span> buf:</span><br><span class="line">                image.save(buf, <span class="built_in">format</span>=<span class="string">&quot;PNG&quot;</span>)</span><br><span class="line">                image_output.append(buf.getvalue())</span><br><span class="line">        torch.cuda.empty_cache()  <span class="comment"># reduce fragmentation</span></span><br><span class="line">        <span class="keyword">return</span> image_output</span><br><span class="line"></span><br><span class="line"><span class="meta">    @modal.web_endpoint(<span class="params">docs=<span class="literal">True</span></span>)</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">web</span>(<span class="params">self, prompt: <span class="built_in">str</span>, width: <span class="built_in">int</span> = <span class="number">768</span>, height: <span class="built_in">int</span> = <span class="number">1024</span>, steps: <span class="built_in">int</span> = <span class="number">28</span>, cfg: <span class="built_in">float</span> = <span class="number">4.5</span>, seed: <span class="built_in">int</span> = <span class="literal">None</span></span>):</span><br><span class="line">        <span class="keyword">return</span> Response(</span><br><span class="line">            content=<span class="variable language_">self</span>.run.local(  <span class="comment"># run in the same container</span></span><br><span class="line">                prompt, width=width, height=height, steps=steps, cfg=cfg, batch_size=<span class="number">1</span>, seed=seed</span><br><span class="line">            )[<span class="number">0</span>],</span><br><span class="line">            media_type=<span class="string">&quot;image/png&quot;</span>,</span><br><span class="line">        )</span><br></pre></td></tr></table></figure><p>要注意两件事，第一，你可以选择不同规格的 GPU,我选择了 A100-40G, 你还可以选择 A100-80G 和 H100。<br>另外，你要去<a href="https://huggingface.co/settings/tokens">hugging face</a>上申请 token, 从而访问只有登陆才能下载的模型。</p><h3 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h3><p>执行命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">modal deploy Flux_cli.py</span><br></pre></td></tr></table></figure><p>稍等片刻，你就可以在你的 models 首页看到自己的 app 了，然后他会给你一个 inference 的链接，加入这个链接是<code>https://XXXXXX</code>, 那么你可以直接打开文档页面获取可以使用的参数</p><p>浏览器访问：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://XXXXXX/docs</span><br></pre></td></tr></table></figure><p>你会发现这是一个 fastapi 搭建的，并且参数是用 get 请求，返回的是图片的二进制数据。</p><p>你可以使用这个 API，搭建一个前端页面来玩转生图啦！</p><h2 id="体验"><a href="#体验" class="headerlink" title="体验"></a>体验</h2><p>前往我搭建好的前端页面体验</p><p><a href="https://sequencetool.reflex.run/freeimage/">freeimage</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;众所周知，Flux 是一个 120 亿参数的大模型，普通人本地根本跑不起来，那么今天我来教大家免费白嫖一个无服务器容器类的网站，帮我们一键化构建一个生图 API, 每个人每个月有免费的 30 刀的赠费，可以使用 H100, A100 等超强显卡资源。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
  </entry>
  
  <entry>
    <title>如何使用 Python 调用 CloudFlare WorkerAI 提供的 Flux 模型</title>
    <link href="https://blog.xiaohanys.top/py-cf-flux/"/>
    <id>https://blog.xiaohanys.top/py-cf-flux/</id>
    <published>2024-10-24T09:16:39.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>前面的文章<a href="https://xiaohanys.top/AI-share1/">打破信息差</a>有提到过，CloudFlare 提供了一些免费的开源模型的 API，而 Flux 的生图模型就在其中。不过，这个模型的 API 最终返回给我们的是 Base64 数据，而不是可直观查看的图片，所以我们需要将图片数据写入文件并上传到云端，然后通过 URL 访问它。另外，我们还希望支持写中文的提示词，所以我们需要在其中增加一个大模型的翻译层，这样就可以把中文的提示词翻译成英文，从而被 Flux 识别。今天，我就通过 Python 代码的方式来实现上述的功能。 🚀</p><span id="more"></span><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>首先，我们当然需要一个 CloudFlare 的账号了，注册账号的教程网络上比比皆是，我就不再此处赘述了。然后，我们需要开通 CloudFlare 的 R2 对象存储，这个对象存储是免费 10G 的，足够我们挥霍了。开通方法可以参考这两篇文章：</p><ul><li><a href="https://yangpeiyuan.com/posts/cloudflare-r2/">使用 Cloudflare R2 免费搭建自用图床</a></li><li><a href="https://www.lihuanyu.com/%E8%BF%90%E7%BB%B4/%E4%BD%BF%E7%94%A8cloudflare-R2%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%9B%BE%E5%BA%8A/">使用 Cloudflare R2 搭建个人图床</a></li></ul><p>然后我们需要记录我们生成的 API_secret 和 API_token，以便后续使用。<br><img src="https://ipfs.xlog.app/ipfs/bafkreigwynnrqkhpqjhbjph452uh623vn3vfkto42gbj3hrrjglygiqixi"><br><img src="https://ipfs.xlog.app/ipfs/bafkreibs7inho3qagrb4hxnj4kieeus3mcf464letsxf36c3eulq5xybpe"></p><p>我们还需要获取我们 CloudFlare 的 account_id 和 worker 的 API_token。</p><h3 id="获取这些值的步骤："><a href="#获取这些值的步骤：" class="headerlink" title="获取这些值的步骤："></a>获取这些值的步骤：</h3><ol><li>登录到 Cloudflare 控制面板并选择您的账户。</li><li>转到 AI &gt; Workers AI。</li><li>选择使用 Workers AI API。</li><li>获取您的 API 令牌：<ul><li>选择获取 API 令牌。</li><li>审查预填信息。</li><li>选择继续到摘要。</li><li>选择创建令牌。</li><li>复制令牌值以备后用。</li></ul></li><li>获取您的账户 ID：<ul><li>回到使用 Workers AI API 的屏幕（您最初的标签页）。</li><li>在获取账户 ID 下，复制账户 ID 的值。</li><li>如果您选择创建一个 API 令牌而不使用模板，该令牌将需要 Workers AI - 读取 和 Workers AI - 编辑 的权限。</li></ul></li></ol><h2 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h2><h3 id="安装必要的包"><a href="#安装必要的包" class="headerlink" title="安装必要的包"></a>安装必要的包</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install openai r2client httpx</span><br></pre></td></tr></table></figure><h3 id="导入相应的模块"><a href="#导入相应的模块" class="headerlink" title="导入相应的模块"></a>导入相应的模块</h3><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> httpx</span><br><span class="line"><span class="keyword">import</span> base64</span><br><span class="line"><span class="keyword">from</span> r2client.R2Client <span class="keyword">import</span> R2Client <span class="keyword">as</span> r2</span><br><span class="line"><span class="keyword">import</span> tempfile</span><br><span class="line"><span class="keyword">import</span> datetime</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> openai <span class="keyword">import</span> OpenAI</span><br></pre></td></tr></table></figure><h3 id="写一个-Python-类实现数据共享"><a href="#写一个-Python-类实现数据共享" class="headerlink" title="写一个 Python 类实现数据共享"></a>写一个 Python 类实现数据共享</h3><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">WorkerImage</span>():</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, account_id, api_token, r2_access_key, r2_secret_key, r2_bucket_name, r2_public_link</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        Parameters:</span></span><br><span class="line"><span class="string">        account_id (str): cloudflare account id</span></span><br><span class="line"><span class="string">        api_token (str): cloudflare api token</span></span><br><span class="line"><span class="string">        r2_access_key (str): r2 access key</span></span><br><span class="line"><span class="string">        r2_secret_key (str): r2 secret key</span></span><br><span class="line"><span class="string">        r2_bucket_name (str): the name of the r2 bucket</span></span><br><span class="line"><span class="string">        r2_public_link (str): the link to access the r2 bucket</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.account_id = account_id</span><br><span class="line">        <span class="variable language_">self</span>.api_token = api_token</span><br><span class="line">        <span class="variable language_">self</span>.endpoint = (</span><br><span class="line">            <span class="string">f&quot;https://api.cloudflare.com/client/v4/accounts/<span class="subst">&#123;account_id&#125;</span>/ai/run/@cf/black-forest-labs/flux-1-schnell&quot;</span></span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">self</span>.headers = &#123;</span><br><span class="line">            <span class="string">&quot;Authorization&quot;</span>: <span class="string">f&quot;Bearer <span class="subst">&#123;api_token&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;Content-Type&quot;</span>: <span class="string">&quot;application/json&quot;</span>,</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="variable language_">self</span>.r2_client = r2(</span><br><span class="line">            access_key=r2_access_key,</span><br><span class="line">            secret_key=r2_secret_key,</span><br><span class="line">            endpoint=<span class="string">f&quot;https://<span class="subst">&#123;account_id&#125;</span>.r2.cloudflarestorage.com&quot;</span>,</span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">self</span>.bucket_name = r2_bucket_name</span><br><span class="line">        <span class="variable language_">self</span>.link = r2_public_link</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_get_flux_image_data</span>(<span class="params">self, prompt, num_steps</span>):</span><br><span class="line">        payload = &#123;</span><br><span class="line">            <span class="string">&quot;prompt&quot;</span>: prompt,</span><br><span class="line">            <span class="string">&quot;num_steps&quot;</span>: num_steps</span><br><span class="line">        &#125;</span><br><span class="line">        response = httpx.post(</span><br><span class="line">            <span class="variable language_">self</span>.endpoint, headers=<span class="variable language_">self</span>.headers, json=payload)</span><br><span class="line">        <span class="keyword">if</span> response.status_code == <span class="number">400</span>:</span><br><span class="line">            <span class="comment"># raise error</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            image_data = base64.b64decode(response.json()[<span class="string">&#x27;result&#x27;</span>][<span class="string">&#x27;image&#x27;</span>])</span><br><span class="line">            <span class="keyword">return</span> image_data</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_save_image_to_tmp_file</span>(<span class="params">self, image_data</span>):</span><br><span class="line">        <span class="keyword">with</span> tempfile.NamedTemporaryFile(suffix=<span class="string">&#x27;.png&#x27;</span>, delete=<span class="literal">False</span>) <span class="keyword">as</span> f:</span><br><span class="line">            f.write(image_data)</span><br><span class="line">            temp_file_path = f.name</span><br><span class="line">        <span class="keyword">return</span> temp_file_path</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">get_translate_promt</span>(<span class="params">self, prompt</span>):</span><br><span class="line">        flux_system_prompt = <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">你是一个基于Flux.1模型的提示词生成机器人。根据用户的需求，自动生成符合Flux.1格式的绘画提示词。虽然你可以参考提供的模板来学习提示词结构和规律，但你必须具备灵活性来应对各种不同需求。最终输出应仅限提示词，无需任何其他解释或信息。你的回答必须全部使用英语进行回复我！</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">### **提示词生成逻辑**：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">1. **需求解析**：从用户的描述中提取关键信息，包括：</span></span><br><span class="line"><span class="string">   - 角色：外貌、动作、表情等。</span></span><br><span class="line"><span class="string">   - 场景：环境、光线、天气等。</span></span><br><span class="line"><span class="string">   - 风格：艺术风格、情感氛围、配色等。</span></span><br><span class="line"><span class="string">   - 其他元素：特定物品、背景或特效。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">2. **提示词结构规律**：</span></span><br><span class="line"><span class="string">   - **简洁、精确且具象**：提示词需要简单、清晰地描述核心对象，并包含足够细节以引导生成出符合需求的图像。</span></span><br><span class="line"><span class="string">   - **灵活多样**：参考下列模板和已有示例，但需根据具体需求生成多样化的提示词，避免固定化或过于依赖模板。</span></span><br><span class="line"><span class="string">   - **符合Flux.1风格的描述**：提示词必须遵循Flux.1的要求，尽量包含艺术风格、视觉效果、情感氛围的描述，使用与Flux.1模型生成相符的关键词和描述模式。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">3. **仅供你参考和学习的几种场景提示词**（你需要学习并灵活调整,&quot;[ ]&quot;中内容视用户问题而定）：</span></span><br><span class="line"><span class="string">   - **角色表情集**：</span></span><br><span class="line"><span class="string">场景说明：适合动画或漫画创作者为角色设计多样的表情。这些提示词可以生成展示同一角色在不同情绪下的表情集，涵盖快乐、悲伤、愤怒等多种情感。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：An anime [SUBJECT], animated expression reference sheet, character design, reference sheet, turnaround, lofi style, soft colors, gentle natural linework, key art, range of emotions, happy sad mad scared nervous embarrassed confused neutral, hand drawn, award winning anime, fully clothed</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[SUBJECT] character, animation expression reference sheet with several good animation expressions featuring the same character in each one, showing different faces from the same person in a grid pattern: happy sad mad scared nervous embarrassed confused neutral, super minimalist cartoon style flat muted kawaii pastel color palette, soft dreamy backgrounds, cute round character designs, minimalist facial features, retro-futuristic elements, kawaii style, space themes, gentle line work, slightly muted tones, simple geometric shapes, subtle gradients, oversized clothing on characters, whimsical, soft puffy art, pastels, watercolor</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **全角度角色视图**：</span></span><br><span class="line"><span class="string">场景说明：当需要从现有角色设计中生成不同角度的全身图时，如正面、侧面和背面，适用于角色设计细化或动画建模。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：A character sheet of [SUBJECT] in different poses and angles, including front view, side view, and back view</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **80 年代复古风格**：</span></span><br><span class="line"><span class="string">场景说明：适合希望创造 80 年代复古风格照片效果的艺术家或设计师。这些提示词可以生成带有怀旧感的模糊宝丽来风格照片。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：blurry polaroid of [a simple description of the scene], 1980s.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **智能手机内部展示**：</span></span><br><span class="line"><span class="string">场景说明：适合需要展示智能手机等产品设计的科技博客作者或产品设计师。这些提示词帮助生成展示手机外观和屏幕内容的图像。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：a iphone product image showing the iphone standing and inside the screen the image is shown</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **双重曝光效果**：</span></span><br><span class="line"><span class="string">场景说明：适合摄影师或视觉艺术家通过双重曝光技术创造深度和情感表达的艺术作品。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：[Abstract style waterfalls, wildlife] inside the silhouette of a [man]’s head that is a double exposure photograph . Non-representational, colors and shapes, expression of feelings, imaginative, highly detailed</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **高质感电影海报**：</span></span><br><span class="line"><span class="string">场景说明：适合需要为电影创建引人注目海报的电影宣传或平面设计师。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：A digital illustration of a movie poster titled [‘Sad Sax: Fury Toad’], [Mad Max] parody poster, featuring [a saxophone-playing toad in a post-apocalyptic desert, with a customized car made of musical instruments], in the background, [a wasteland with other musical vehicle chases], movie title in [a gritty, bold font, dusty and intense color palette].</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **镜面自拍效果**：</span></span><br><span class="line"><span class="string">场景说明：适合想要捕捉日常生活瞬间的摄影师或社交媒体用户。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：Phone photo: A woman stands in front of a mirror, capturing a selfie. The image quality is grainy, with a slight blur softening the details. The lighting is dim, casting shadows that obscure her features. [The room is cluttered, with clothes strewn across the bed and an unmade blanket. Her expression is casual, full of concentration], while the old iPhone struggles to focus, giving the photo an authentic, unpolished feel. The mirror shows smudges and fingerprints, adding to the raw, everyday atmosphere of the scene.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **像素艺术创作**：</span></span><br><span class="line"><span class="string">场景说明：适合像素艺术爱好者或复古游戏开发者创造或复刻经典像素风格图像。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">提示词：[Anything you want] pixel art style, pixels, pixel art</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   - **以上部分场景仅供你学习，一定要学会灵活变通，以适应任何绘画需求**：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">4. **Flux.1提示词要点总结**：</span></span><br><span class="line"><span class="string">   - **简洁精准的主体描述**：明确图像中核心对象的身份或场景。</span></span><br><span class="line"><span class="string">   - **风格和情感氛围的具体描述**：确保提示词包含艺术风格、光线、配色、以及图像的氛围等信息。</span></span><br><span class="line"><span class="string">   - **动态与细节的补充**：提示词可包括场景中的动作、情绪、或光影效果等重要细节。</span></span><br><span class="line"><span class="string">   - **其他更多规律请自己寻找**</span></span><br><span class="line"><span class="string">---</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">**问答案例1**：</span></span><br><span class="line"><span class="string">**用户输入**：一个80年代复古风格的照片。</span></span><br><span class="line"><span class="string">**你的输出**：A blurry polaroid of a 1980s living room, with vintage furniture, soft pastel tones, and a nostalgic, grainy texture,  The sunlight filters through old curtains, casting long, warm shadows on the wooden floor, 1980s,</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">**问答案例2**：</span></span><br><span class="line"><span class="string">**用户输入**：一个赛博朋克风格的夜晚城市背景</span></span><br><span class="line"><span class="string">**你的输出**：A futuristic cityscape at night, in a cyberpunk style, with neon lights reflecting off wet streets, towering skyscrapers, and a glowing, high-tech atmosphere. Dark shadows contrast with vibrant neon signs, creating a dramatic, dystopian mood</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">        client = OpenAI(</span><br><span class="line">            <span class="comment"># This is the default and can be omitted</span></span><br><span class="line">            api_key=<span class="variable language_">self</span>.api_token,</span><br><span class="line">            base_url=<span class="string">f&quot;https://api.cloudflare.com/client/v4/accounts/<span class="subst">&#123;self.account_id&#125;</span>/ai/v1&quot;</span>,</span><br><span class="line">            max_retries=<span class="number">5</span>,</span><br><span class="line">        )</span><br><span class="line">        chat_completion = client.chat.completions.create(</span><br><span class="line">            messages=[</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="string">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;content&quot;</span>: flux_system_prompt,</span><br><span class="line">                &#125;,</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;content&quot;</span>: prompt,</span><br><span class="line">                &#125;</span><br><span class="line">            ],</span><br><span class="line">            model=<span class="string">&quot;@cf/meta/llama-3.1-70b-instruct&quot;</span>,</span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">return</span> chat_completion.choices[<span class="number">0</span>].message.content</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">get_image</span>(<span class="params">self, prompt, num_steps</span>):</span><br><span class="line">        image_data = <span class="variable language_">self</span>._get_flux_image_data(prompt, num_steps)</span><br><span class="line">        <span class="keyword">if</span> image_data:</span><br><span class="line">            local_file = <span class="variable language_">self</span>._save_image_to_tmp_file(image_data)</span><br><span class="line">            <span class="comment"># delete local file</span></span><br><span class="line">            <span class="comment"># get date time as newfile name</span></span><br><span class="line">            newfilename = <span class="string">f&quot;<span class="subst">&#123;datetime.datetime.now().strftime(<span class="string">&#x27;%Y-%m-%d-%H-%M-%S&#x27;</span>)&#125;</span>.png&quot;</span></span><br><span class="line">            <span class="variable language_">self</span>.r2_client.upload_file(</span><br><span class="line">                <span class="variable language_">self</span>.bucket_name, local_file, newfilename)</span><br><span class="line">            os.remove(local_file)</span><br><span class="line">            <span class="keyword">return</span> <span class="string">f&quot;<span class="subst">&#123;self.link&#125;</span>/<span class="subst">&#123;newfilename&#125;</span>&quot;</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;image data is None,please check your inputs&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><p>首先初始化类：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">work = WorkerImage(account_id, api_token, r2_access_key, r2_secret_key, r2_bucket_name, r2_public_link)</span><br></pre></td></tr></table></figure><p>注意，我们要把准备工作里获取的这些值填入 <code>WorkerImage</code> 类中来初始化它。</p><ul><li><code>account_id</code>：CloudFlare 账户 ID</li><li><code>api_token</code>：CloudFlare Worker 的 API token</li><li><code>r2_access_key</code>：CloudFlare R2 的 S3 access id</li><li><code>r2_secret_key</code>：CloudFlare R2 的 S3 access key</li><li><code>r2_bucket_name</code>：CloudFlare R2 的 S3 存储桶名（你自己手动创建的）</li><li><code>r2_public_link</code>：CloudFlare R2 的访问链接，如果你设置了自定义域可以选择你自己的或者它提供的 <code>dev</code> 结尾的域名</li></ul><p>然后，我们首先获取翻译后的提示词，调用：</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line">newprompt = worker.get_translate_promt(prompt=<span class="string">&quot;一个80年代复古风格的照片,一个男孩手里拿着一罐可乐站在长城上看向镜头.&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(newprompt)</span><br><span class="line"></span><br><span class="line"><span class="comment">##a blurry polaroid photograph of a young boy standing on the Great Wall of China, holding a can of soda, with a curious, blissful expression on his face, wearing a red hat, and a backpack, set against the rugged, ancient stones, and the misty, mountains in the background, soft pastel tones, warm, nostalgic lighting, with a subtle, retro film grain, taken in the 1980s.</span></span><br></pre></td></tr></table></figure><p>下一步则把这个提示词输入到画图程序中即可：</p><figure class="highlight python"><table><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">url = worker.get_image(prompt=newprompt, num_steps=<span class="number">8</span>)</span><br><span class="line"><span class="built_in">print</span>(url)</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我们这个类其实就是一个综合性的管道，首先调用 WorkerAI 的文本生成大模型，将用户输入的中文提示词翻译为英文，并美化增加细节。然后我们将生成的提示词给 Flux 大模型来画图即可。通过这种方式，我们可以轻松地生成并上传图片，同时支持中文提示词。 🎨✨</p><h2 id="Bonus-time"><a href="#Bonus-time" class="headerlink" title="Bonus time"></a>Bonus time</h2><p>我将上述代码部署到了我自己的网站，现在你可以通过我的网站来体验该过程：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://sequencetool.reflex.run/freeimage/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://sequencetool.reflex.run/freeimage/</span><span class="cap link footnote">https://sequencetool.reflex.run/freeimage/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>完整的代码也可以通过谷歌的 colab 访问：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://colab.research.google.com/drive/1CPjBq2soIryLEWzxW-6rutgyw_iZA6UI?usp=sharing" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://colab.research.google.com/drive/1CPjBq2soIryLEWzxW-6rutgyw_iZA6UI?usp=sharing</span><span class="cap link footnote">https://colab.research.google.com/drive/1CPjBq2soIryLEWzxW-6rutgyw_iZA6UI?usp=sharing</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;前面的文章&lt;a href=&quot;https://xiaohanys.top/AI-share1/&quot;&gt;打破信息差&lt;/a&gt;有提到过，CloudFlare 提供了一些免费的开源模型的 API，而 Flux 的生图模型就在其中。不过，这个模型的 API 最终返回给我们的是 Base64 数据，而不是可直观查看的图片，所以我们需要将图片数据写入文件并上传到云端，然后通过 URL 访问它。另外，我们还希望支持写中文的提示词，所以我们需要在其中增加一个大模型的翻译层，这样就可以把中文的提示词翻译成英文，从而被 Flux 识别。今天，我就通过 Python 代码的方式来实现上述的功能。 🚀&lt;/p&gt;</summary>
    
    
    
    <category term="编程语言" scheme="https://blog.xiaohanys.top/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
    <category term="网站分享" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E5%88%86%E4%BA%AB/"/>
    
  </entry>
  
  <entry>
    <title>2024最新免费使用Flux全模型网站分享</title>
    <link href="https://blog.xiaohanys.top/flux-new-model/"/>
    <id>https://blog.xiaohanys.top/flux-new-model/</id>
    <published>2024-10-18T05:12:00.000Z</published>
    <updated>2026-03-26T09:21:36.505Z</updated>
    
    <content type="html"><![CDATA[<p>在快速发展的 AI 图像生成领域，出现了一个新的参与者，它将重新定义 AI 绘画的可能性。Flux.1 就是由黑森林实验室开发的一套开创性的模型，它正在席卷 AI 社区。让我们深入了解 Flux.1 的特别之处，以及它为何被誉为图像合成领域的新王？</p><span id="more"></span><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:400px;"><img class="lazy" src="https://svd.pixarai.net/2024/08/033e5475f103005c5c38c1df2aeae457.webp" data-src="https://svd.pixarai.net/2024/08/033e5475f103005c5c38c1df2aeae457.webp"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><h2 id="何为-Flux-1"><a href="#何为-Flux-1" class="headerlink" title="何为 Flux.1 ?"></a>何为 Flux.1 ?</h2><p>Flux.1 不仅仅是另一个 AI 模型，它是由 Black Forest Labs(黑森林实验室) 开发的一系列最先进的文本转图像生成模型。Black Forest Labs 由一群杰出的 AI 研究人员和工程师创立，其中包括 Stable Diffusion 的创建者，致力于推动生成 AI 的发展。</p><p>Flux.1 套件包含三个版本：</p><ul><li>Flux.1 [pro]：旗舰型号，在图像生成方面提供无与伦比的性能。</li><li>Flux.1 [dev]：针对非商业应用的开放权重模型。</li><li>Flux.1 [schnell]：最快的型号，专为本地开发和个人使用而设计。</li></ul><p>每个版本都针对不同的用例进行了定制，确保无论您是普通用户还是专业开发人员，都有适合您需求的 Flux.1 模型。</p><h2 id="背后的技术"><a href="#背后的技术" class="headerlink" title="背后的技术"></a>背后的技术</h2><p>Flux.1 的核心是混合架构，它结合了多模态和并行扩散变压器块。Flux.1 可扩展到令人印象深刻的 120 亿个参数，利用流匹配、旋转位置嵌入和并行注意层等先进技术来实现其卓越的性能。</p><p>这种创新方法使 Flux.1 在图像生成的各个方面表现出色，包括：</p><ul><li>视觉质量</li><li>及时遵守</li><li>尺寸和长宽比变化</li><li>排版</li><li>输出分集</li></ul><h2 id="Flux-1-为何脱颖而出"><a href="#Flux-1-为何脱颖而出" class="headerlink" title="Flux.1 为何脱颖而出"></a>Flux.1 为何脱颖而出</h2><ul><li><strong>无与伦比的性能</strong>：Flux.1 [pro] 和 [dev] 在多个基准测试中的表现优于 Midjourney v6.0、DALL·E 3 (HD) 和 SD3-Ultra 等热门型号。</li><li><strong>多功能性</strong>：通过支持各种宽高比和分辨率，Flux.1 在图像创建方面提供了前所未有的灵活性。</li><li><strong>速度</strong>：Flux.1 [schnell] 是目前最先进的快速模型，可在不影响质量的情况下快速生成图像。</li><li><strong>开源</strong>：[dev] 和 [schnell] 版本是开源的，促进了 AI 社区内的创新和协作。</li><li><strong>企业解决方案</strong>：对于希望利用尖端人工智能的企业，Flux.1 [pro] 提供专用和定制的企业解决方案。</li></ul><h2 id="如何开始使用-Flux-1"><a href="#如何开始使用-Flux-1" class="headerlink" title="如何开始使用 Flux.1"></a>如何开始使用 Flux.1</h2><p>准备好体验 AI 图像生成的未来了吗？您可以按照以下方法开始：</p><ul><li>对于 Flux.1 [pro] 去官方获取 API</li><li>在 Hugging Face 上尝试 Flux.1 [dev] 或者 Replicate</li><li>从 GitHub 下载 Flux.1 [schnell] 供本地使用</li></ul><p>今天我们推荐一个目前来说完全免费的网站可以完全使用三个模型，无论是 Pro 亦或者 schnell, 都是免费无限制的，速速体验。</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://www.fluximageai.com/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://www.fluximageai.com/</span><span class="cap link footnote">https://www.fluximageai.com/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>Flux.1 代表了 AI 图像生成技术的重大飞跃。它结合了先进的架构、多功能模型和开源可用性，使其成为该领域的游戏规则改变者。无论您是 AI 爱好者、专业开发人员还是希望利用生成 AI 力量的企业，Flux.1 都能提供满足甚至超出您期望的解决方案。</p><p>随着人工智能领域的不断发展，有一点是明确的：Flux.1 正引领我们进入一个充满创意可能性的新时代。若想站在这项激动人心的技术的前沿，请务必访问<a href="https://www.fluximageai.com/">https://www.fluximageai.com</a>探索 Flux.1 ，并加入塑造人工智能生成媒体未来的创新者社区。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在快速发展的 AI 图像生成领域，出现了一个新的参与者，它将重新定义 AI 绘画的可能性。Flux.1 就是由黑森林实验室开发的一套开创性的模型，它正在席卷 AI 社区。让我们深入了解 Flux.1 的特别之处，以及它为何被誉为图像合成领域的新王？&lt;/p&gt;</summary>
    
    
    
    <category term="网站分享" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E5%88%86%E4%BA%AB/"/>
    
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
    <category term="网站分享" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E5%88%86%E4%BA%AB/"/>
    
  </entry>
  
  <entry>
    <title>小白如何用reflex搭建一个AI应用?</title>
    <link href="https://blog.xiaohanys.top/reflex-ai-app/"/>
    <id>https://blog.xiaohanys.top/reflex-ai-app/</id>
    <published>2024-10-16T09:12:34.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<div class="tag-plugin colorful note" color="green"><div class="title">我们在一年前启动了</div><div class="body"><p>Reflex，以便任何了解 Python 的人都可以轻松构建 Web 应用程序并与世界分享，而无需学习新语言和拼凑一堆不同的工具。</p></div></div><div class="tag-plugin colorful note" color="blue"><div class="title">开发是最流行的编程用例之一。</div><div class="body"><p>Python 是世界上最流行的编程语言之一。那么为什么我们不能用 Python 构建 Web 应用程序呢？</p></div></div><p>以上来自 Reflex 构建者发布的博客，原文链接如下：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://reflex.dev/blog/2024-03-21-reflex-architecture/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://reflex.dev/blog/2024-03-21-reflex-architecture/</span><span class="cap link footnote">https://reflex.dev/blog/2024-03-21-reflex-architecture/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><span id="more"></span><p>所以真的有一种感觉，Reflex 使用一种语言让我们小白可以直接做到以前需要整合 Html, Javascript, Python 等众多语言才能构建的复杂应用，简直是神奇！那么今天，我来介绍一下我自己搭建的基于免费 API 的 AI 工具的。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1280/570;"><img class="lazy" src="https://i.postimg.cc/wBmbsmQf/2024-10-16-17-20-33.png" data-src="https://i.postimg.cc/wBmbsmQf/2024-10-16-17-20-33.png" alt="首先看一下默认界面"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">首先看一下默认界面</span></div></div><p>首先是 AI 绘画应用，</p><h3 id="功能介绍："><a href="#功能介绍：" class="headerlink" title="功能介绍："></a>功能介绍：</h3><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>目前一共支持 4 个模型，其中Flux系列是最好的，Flux-dev是收费的</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>支持魔术提示词，即使输入中文，也可以翻译成合适且有想象力的提示词</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>支持不同尺寸图形绘制</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox" checked="true"/><span>支持高级选项，例如负面提示词，步数和cfg_scale</span></div><div class="tag-plugin colorful checkbox" ><input type="checkbox"/><span>懒，没有优化手机端，所以必须用电脑，否则，页面会乱</span></div><h3 id="如何使用："><a href="#如何使用：" class="headerlink" title="如何使用："></a>如何使用：</h3><p>免费版的 API 有速率限制，我一开始也想直接内置我的算了，后来发现不行，于是修改为可以手动设置自己的 API，而获取 API 也很简单，只需要去到<a href="https://cloud.siliconflow.cn/">SiliconFlow 官网</a>就可以获取了。</p><p>而魔术提示词（Magic Prompt）是需要 OpenAI 提供的大语言模型来实现翻译并优化的，或者使用 OpenAI 兼容的 API 端点。这里推荐大家使用</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://api1.zhtec.xyz/::https://api.zhtec.xyz/xht1.png" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://api1.zhtec.xyz/::https://api.zhtec.xyz/xht1.png</span><span class="cap link footnote">https://api1.zhtec.xyz/::https://api.zhtec.xyz/xht1.png</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>提供的 API，GPT4 的价格都非常便宜。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1280/590;"><img class="lazy" src="https://i.postimg.cc/G2ngPmMc/2024-10-16-17-03-30.png" data-src="https://i.postimg.cc/G2ngPmMc/2024-10-16-17-03-30.png" alt="设置界面如图所示"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">设置界面如图所示</span></div></div><p>其中：</p><ul><li>SiliconFlow API Key: 就是从 siliconflow 获取的 API 的</li><li>Magic Prompt endpoint: OpenAI（GPT）兼容的 API 端点链接</li><li>Magic Prompt API key: GPT 模型对应的 API</li><li>Magic Prompt Model: 你打算用哪个模型来优化提示词，gpt-4o 最强，但是贵！！！</li></ul><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><p>当你设置好以上信息，这些信息会被储存到你自己的浏览器中，然后下次再打开就不用重复输入了。然后我们尝试输入一下提示词看看：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1280/605;"><img class="lazy" src="https://i.postimg.cc/fL728QFY/2024-10-16-17-00-30.png" data-src="https://i.postimg.cc/fL728QFY/2024-10-16-17-00-30.png" alt="虽然输入了中文，但是画面依旧很优秀"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">虽然输入了中文，但是画面依旧很优秀</span></div></div><p>直达链接:</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://sequencetool.reflex.run/aiart/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://sequencetool.reflex.run/aiart/</span><span class="cap link footnote">https://sequencetool.reflex.run/aiart/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><p>至于 AI 对话应用就和 AI 绘画是相同的逻辑，你只需要获取 API 就可以了，没有别的要注意的。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1280/600;"><img class="lazy" src="https://i.postimg.cc/kgF2Mjkf/2024-10-16-17-02-12.png" data-src="https://i.postimg.cc/kgF2Mjkf/2024-10-16-17-02-12.png" alt="聊天页面"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">聊天页面</span></div></div><p>直达链接:</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://sequencetool.reflex.run/aichat/" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://sequencetool.reflex.run/aichat/</span><span class="cap link footnote">https://sequencetool.reflex.run/aichat/</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div><h3 id="源代码"><a href="#源代码" class="headerlink" title="源代码"></a>源代码</h3><p>因为实现逻辑比较复杂，所以在本文就不再详细的介绍实现的原理了，感兴趣可以查看代码：</p><div class="tag-plugin link dis-select"><a class="link-card plain" title="" href="https://github.com/panxiaoguang/SequenceTool" target="_blank" rel="external nofollow noopener noreferrer" cardlink autofill="title,icon,desc"><div class="left"><span class="title">https://github.com/panxiaoguang/SequenceTool</span><span class="cap link footnote">https://github.com/panxiaoguang/SequenceTool</span></div><div class="right"><div class="lazy img" data-bg="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg"></div></div></a></div>]]></content>
    
    
    <summary type="html">&lt;div class=&quot;tag-plugin colorful note&quot; color=&quot;green&quot;&gt;&lt;div class=&quot;title&quot;&gt;我们在一年前启动了&lt;/div&gt;&lt;div class=&quot;body&quot;&gt;&lt;p&gt;Reflex，以便任何了解 Python 的人都可以轻松构建 Web 应用程序并与世界分享，而无需学习新语言和拼凑一堆不同的工具。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;tag-plugin colorful note&quot; color=&quot;blue&quot;&gt;&lt;div class=&quot;title&quot;&gt;开发是最流行的编程用例之一。&lt;/div&gt;&lt;div class=&quot;body&quot;&gt;&lt;p&gt;Python 是世界上最流行的编程语言之一。那么为什么我们不能用 Python 构建 Web 应用程序呢？&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;以上来自 Reflex 构建者发布的博客，原文链接如下：&lt;/p&gt;
&lt;div class=&quot;tag-plugin link dis-select&quot;&gt;&lt;a class=&quot;link-card plain&quot; title=&quot;&quot; href=&quot;https://reflex.dev/blog/2024-03-21-reflex-architecture/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow noopener noreferrer&quot; cardlink autofill=&quot;title,icon,desc&quot;&gt;&lt;div class=&quot;left&quot;&gt;&lt;span class=&quot;title&quot;&gt;https://reflex.dev/blog/2024-03-21-reflex-architecture/&lt;/span&gt;&lt;span class=&quot;cap link footnote&quot;&gt;https://reflex.dev/blog/2024-03-21-reflex-architecture/&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;right&quot;&gt;&lt;div class=&quot;lazy img&quot; data-bg=&quot;https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/link/8f277b4ee0ecd.svg&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="AI" scheme="https://blog.xiaohanys.top/tags/AI/"/>
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>如何在Reflex里面使用IGV浏览器？</title>
    <link href="https://blog.xiaohanys.top/reflex-igv/"/>
    <id>https://blog.xiaohanys.top/reflex-igv/</id>
    <published>2024-10-09T04:49:48.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<div class="tag-plugin colorful note" color="blue"><div class="title">Reflex</div><div class="body"><p>是一个开源库，旨在使用纯 Python 构建全栈 Web 应用程序。它的设计理念是让开发者能够完全使用 Python 编写应用的前端和后端，无需学习 JavaScript。这对于那些希望专注于 Python 编程语言的开发者来说是一个重大的福音。</p></div></div><p>上面说的都是场面话，这都依托于它自身包装的第三方应用够丰富。但是，对于一些冷门的应用，比如我这里要说的 IGV 浏览器，就没有那么容易了。这里我就来分享一下我是如何在 Reflex 里面使用 IGV 浏览器的。</p><span id="more"></span><h3 id="包装-igv-js-到-react-组件"><a href="#包装-igv-js-到-react-组件" class="headerlink" title="包装 igv.js 到 react 组件"></a>包装 igv.js 到 react 组件</h3><p>首先，我们需要把 igv.js 包装成一个 React 组件。我们以两个需要实现的交互为例子来说明：</p><ul><li>第一个是我们有一个外部的控制器，可以通过这个控制器来控制 igv.js 显示的坐标范围。</li><li>第二个是当我们鼠标点击某个区域的时候，我们可以获取到这个区域的所有信息。</li></ul><p>首先，通过查阅 IGV.js 的<a href="https://igv.org/doc/igvjs/#">文档</a>，我们知道，我们可以通过 <code>igv.createBrowser</code> 来创建一个 IGV 浏览器。我们可以通过这个函数来创建一个 IGV 浏览器，然后通过这个浏览器的实例来控制 IGV 的显示。</p><figure class="highlight javascript"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> igv <span class="keyword">from</span> <span class="string">&quot;https://cdn.jsdelivr.net/npm/igv@3.0.0/dist/igv.esm.min.js&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> div = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;igv_div&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> config = &#123;</span><br><span class="line">  <span class="attr">genome</span>: <span class="string">&quot;hg19&quot;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> browser = <span class="keyword">await</span> igv.<span class="title function_">createBrowser</span>(div, config);</span><br></pre></td></tr></table></figure><p>然后我们可以通过<code>igv.removeAllBrowsers()</code>来移除所有的浏览器。</p><p>那么，我们放到 React 组件里面，就是这样的：</p><figure class="highlight javascript"><table><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">import</span> <span class="title class_">React</span>, &#123; useEffect, useRef &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> igv <span class="keyword">from</span> <span class="string">&quot;igv&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">IgvComponent</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> igvContainerRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> browserRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> options = &#123;</span><br><span class="line">      <span class="attr">genome</span>: <span class="string">&quot;hg19&quot;</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">    igv.<span class="title function_">createBrowser</span>(igvContainerRef.<span class="property">current</span>, options).<span class="title function_">then</span>(<span class="function">(<span class="params">browser</span>) =&gt;</span> &#123;</span><br><span class="line">      browserRef.<span class="property">current</span> = browser;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">ref</span>=<span class="string">&#123;igvContainerRef&#125;</span> /&gt;</span>;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">IgvComponent</span>;</span><br></pre></td></tr></table></figure><p>现在，我们来添加交互，我们想要实现的第二个功能需要 <code>browser.on</code>来监听事件。比如我们想要监听鼠标点击事件，我们可以这样：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">browser.<span class="title function_">on</span>(<span class="string">&quot;trackclick&quot;</span>, <span class="function">(<span class="params">track, popoverData</span>) =&gt;</span> &#123;&#125;);</span><br></pre></td></tr></table></figure><p>我们放入 React 组件里面，就是这样的：</p><figure class="highlight javascript"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useEffect, useRef &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> igv <span class="keyword">from</span> <span class="string">&quot;igv&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">IgvComponent</span> = (<span class="params">genome, locus, tracks, onTrackClick</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> igvContainerRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> browserRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> options = &#123;</span><br><span class="line">      genome,</span><br><span class="line">      locus,</span><br><span class="line">      tracks,</span><br><span class="line">    &#125;;</span><br><span class="line">    igv.<span class="title function_">createBrowser</span>(igvContainerRef.<span class="property">current</span>, options).<span class="title function_">then</span>(<span class="function">(<span class="params">browser</span>) =&gt;</span> &#123;</span><br><span class="line">      browserRef.<span class="property">current</span> = browser;</span><br><span class="line">      <span class="keyword">if</span> (onTrackClick) &#123;</span><br><span class="line">        browser.<span class="title function_">on</span>(<span class="string">&quot;trackclick&quot;</span>, <span class="function">(<span class="params">track, popoverData</span>) =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> trackName = track.<span class="property">name</span>;</span><br><span class="line">          <span class="keyword">const</span> trackValue = popoverData.<span class="title function_">map</span>(<span class="function">(<span class="params">data</span>) =&gt;</span> (&#123;</span><br><span class="line">            <span class="attr">name</span>: data.<span class="property">name</span>,</span><br><span class="line">            <span class="attr">value</span>: data.<span class="property">value</span>,</span><br><span class="line">          &#125;));</span><br><span class="line">          <span class="title function_">onTrackClick</span>(trackName, trackValue);</span><br><span class="line">        &#125;);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">ref</span>=<span class="string">&#123;igvContainerRef&#125;</span> /&gt;</span>;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">IgvComponent</span>;</span><br></pre></td></tr></table></figure><p>而第一个要求里，我们要外部控制，需要获取到 browser 的实例，然后通过这个实例来控制 IGV 的显示。我们可以通过 <code>browser.search</code> 来搜索某个基因，通过 <code>browser.zoomIn</code> 和 <code>browser.zoomOut</code> 来缩放。这在 react 里是这样的：</p><figure class="highlight javascript"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useEffect, useRef &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> igv <span class="keyword">from</span> <span class="string">&quot;../node_modules/igv/dist/igv.esm.min.js&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">IgvComponent</span> = (<span class="params">&#123; genome, locus, tracks, onTrackClick &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> igvContainerRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> browserRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> options = &#123;</span><br><span class="line">      genome,</span><br><span class="line">      locus,</span><br><span class="line">      tracks,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!browserRef.<span class="property">current</span>) &#123;</span><br><span class="line">      igv.<span class="title function_">createBrowser</span>(igvContainerRef.<span class="property">current</span>, options).<span class="title function_">then</span>(<span class="function">(<span class="params">browser</span>) =&gt;</span> &#123;</span><br><span class="line">        browserRef.<span class="property">current</span> = browser;</span><br><span class="line">        <span class="keyword">if</span> (onTrackClick) &#123;</span><br><span class="line">          browser.<span class="title function_">on</span>(<span class="string">&quot;trackclick&quot;</span>, <span class="function">(<span class="params">track, popoverData</span>) =&gt;</span> &#123;</span><br><span class="line">            <span class="keyword">const</span> trackName = track.<span class="property">name</span>;</span><br><span class="line">            <span class="keyword">const</span> trackValue = popoverData.<span class="title function_">map</span>(<span class="function">(<span class="params">data</span>) =&gt;</span> (&#123;</span><br><span class="line">              <span class="attr">name</span>: data.<span class="property">name</span>,</span><br><span class="line">              <span class="attr">value</span>: data.<span class="property">value</span>,</span><br><span class="line">            &#125;));</span><br><span class="line">            <span class="title function_">onTrackClick</span>(trackName, trackValue);</span><br><span class="line">          &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 清理函数，在组件卸载时调用</span></span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (browserRef.<span class="property">current</span>) &#123;</span><br><span class="line">        igv.<span class="title function_">removeAllBrowsers</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, []);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (browserRef.<span class="property">current</span>) &#123;</span><br><span class="line">      browserRef.<span class="property">current</span>.<span class="title function_">search</span>(locus);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [locus]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">ref</span>=<span class="string">&#123;igvContainerRef&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">IgvComponent</span>;</span><br></pre></td></tr></table></figure><p>这样，我们就完成了最终的包装。我们可以自己搭建 React 环境来测试一下是否可行，或者可以去找在线平台测试，例如<a href="https://playcode.io/">playcode</a></p><h3 id="在-Reflex-里面使用"><a href="#在-Reflex-里面使用" class="headerlink" title="在 Reflex 里面使用"></a>在 Reflex 里面使用</h3><p>根据官方文档的表述，React 组件的包装就相当简单了，我们这里是本地的组件，所以就可以这样：</p><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">### IGV.py</span></span><br><span class="line"><span class="keyword">import</span> reflex <span class="keyword">as</span> rx</span><br><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">List</span>, <span class="type">Dict</span></span><br><span class="line"><span class="keyword">from</span> reflex.components.component <span class="keyword">import</span> NoSSRComponent</span><br><span class="line"></span><br><span class="line"><span class="comment">### 一定要注意这个只可以在客户端渲染，不支持服务端</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">IGV</span>(<span class="title class_ inherited__">NoSSRComponent</span>):</span><br><span class="line">    <span class="comment">###官方文档告诉我们，我们的组件需要放在assets文件夹下,但是这里要写public</span></span><br><span class="line">    library = <span class="string">&quot;/public/igv&quot;</span></span><br><span class="line"></span><br><span class="line">    tag = <span class="string">&quot;IgvComponent&quot;</span></span><br><span class="line"></span><br><span class="line">    is_default = <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">    lib_dependencies: <span class="built_in">list</span>[<span class="built_in">str</span>] = [</span><br><span class="line">        <span class="string">&quot;igv@3.0.6&quot;</span></span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line">    genome: rx.Var[<span class="built_in">str</span>]</span><br><span class="line">    locus: rx.Var[<span class="built_in">str</span>]</span><br><span class="line">    tracks: rx.Var[<span class="type">List</span>[<span class="type">Dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>]]]</span><br><span class="line"></span><br><span class="line">    on_track_click: rx.EventHandler[<span class="keyword">lambda</span> e0, e1: [e0, e1]]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">igvComponent = IGV.create</span><br></pre></td></tr></table></figure><p>一定要注意的是，Reflex 默认是服务端渲染的，但是显然 IGV 浏览器无法在服务端兼容，那么我们就是用 <code>NoSSRComponent</code>来包装。</p><h3 id="如何使用？"><a href="#如何使用？" class="headerlink" title="如何使用？"></a>如何使用？</h3><p>我们可以写一个小的 Demo 来测试一下：</p><figure class="highlight python"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> reflex <span class="keyword">as</span> rx</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> .IGV <span class="keyword">import</span> igvComponent</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">State</span>(rx.State):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;The app state.&quot;&quot;&quot;</span></span><br><span class="line">    genome: <span class="built_in">str</span> = <span class="string">&quot;hg38&quot;</span></span><br><span class="line">    loc: <span class="built_in">str</span> = <span class="string">&quot;chr8:127736588-127739371&quot;</span></span><br><span class="line">    tracks: <span class="built_in">list</span>[<span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>]] = [</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="string">&quot;name&quot;</span>: <span class="string">&quot;HG00103&quot;</span>,</span><br><span class="line">            <span class="string">&quot;url&quot;</span>: <span class="string">&quot;https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram&quot;</span>,</span><br><span class="line">            <span class="string">&quot;indexURL&quot;</span>: <span class="string">&quot;https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram.crai&quot;</span>,</span><br><span class="line">            <span class="string">&quot;format&quot;</span>: <span class="string">&quot;cram&quot;</span></span><br><span class="line">        &#125;]</span><br><span class="line">    location: <span class="built_in">str</span></span><br><span class="line">    trackName: <span class="built_in">str</span></span><br><span class="line">    trackdata: <span class="built_in">list</span>[<span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>]]</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">change_loc</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="variable language_">self</span>.loc = <span class="variable language_">self</span>.location</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">get_click_info</span>(<span class="params">self, e0: <span class="built_in">str</span>, e1: <span class="built_in">list</span>[<span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>]]</span>):</span><br><span class="line">        <span class="variable language_">self</span>.trackName = e0</span><br><span class="line">        <span class="variable language_">self</span>.trackdata = [&#123;<span class="string">&quot;name&quot;</span>: itm[<span class="string">&#x27;name&#x27;</span>], <span class="string">&quot;value&quot;</span>: itm[<span class="string">&#x27;value&#x27;</span>]&#125;</span><br><span class="line">                          <span class="keyword">for</span> itm <span class="keyword">in</span> e1 <span class="keyword">if</span> itm[<span class="string">&#x27;name&#x27;</span>]]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">data_item</span>(<span class="params">name: <span class="built_in">str</span>, value: <span class="built_in">str</span></span>) -&gt; rx.Component:</span><br><span class="line">    <span class="keyword">return</span> rx.data_list.item(</span><br><span class="line">        rx.data_list.label(name),</span><br><span class="line">        rx.data_list.value(value)</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">data_card</span>(<span class="params">trackdata: <span class="built_in">list</span>[<span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>]]</span>) -&gt; rx.Component:</span><br><span class="line">    <span class="keyword">return</span> rx.card(</span><br><span class="line">        rx.data_list.root(</span><br><span class="line">            rx.foreach(trackdata, <span class="keyword">lambda</span> item: data_item(</span><br><span class="line">                item[<span class="string">&#x27;name&#x27;</span>], item[<span class="string">&#x27;value&#x27;</span>]))</span><br><span class="line">        )</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">index</span>() -&gt; rx.Component:</span><br><span class="line">    <span class="comment"># Welcome Page (Index)</span></span><br><span class="line">    <span class="keyword">return</span> rx.container(</span><br><span class="line">        rx.vstack(</span><br><span class="line">            rx.heading(<span class="string">&quot;IGV test!&quot;</span>, size=<span class="string">&quot;9&quot;</span>),</span><br><span class="line">            rx.box(</span><br><span class="line">                igvComponent(genome=State.genome, locus=State.loc,</span><br><span class="line">                             tracks=State.tracks, on_track_click=State.get_click_info),</span><br><span class="line">                width=<span class="string">&quot;100%&quot;</span>,</span><br><span class="line">            ),</span><br><span class="line">            rx.hstack(</span><br><span class="line">                rx.<span class="built_in">input</span>(placeholder=<span class="string">&quot;Enter location&quot;</span>,</span><br><span class="line">                         on_blur=State.set_location,</span><br><span class="line">                         width=<span class="string">&quot;40%&quot;</span>),</span><br><span class="line">                rx.button(<span class="string">&quot;GoTo!&quot;</span>, on_click=State.change_loc),</span><br><span class="line">                width=<span class="string">&quot;100%&quot;</span>,</span><br><span class="line">                justify=<span class="string">&quot;center&quot;</span></span><br><span class="line">            ),</span><br><span class="line">            rx.divider(),</span><br><span class="line">            rx.cond(</span><br><span class="line">                State.trackName,</span><br><span class="line">                rx.vstack(</span><br><span class="line">                    rx.heading(State.trackName, size=<span class="string">&quot;4&quot;</span>),</span><br><span class="line">                    data_card(State.trackdata)</span><br><span class="line">                ),</span><br><span class="line">                rx.heading(<span class="string">&quot;No track clicked&quot;</span>, size=<span class="string">&quot;4&quot;</span>)</span><br><span class="line">            ),</span><br><span class="line"></span><br><span class="line">            spacing=<span class="string">&quot;5&quot;</span>,</span><br><span class="line">            justify=<span class="string">&quot;center&quot;</span>,</span><br><span class="line">            min_height=<span class="string">&quot;85vh&quot;</span>,</span><br><span class="line">        ),</span><br><span class="line">        rx.logo()</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">app = rx.App()</span><br><span class="line">app.add_page(index)</span><br></pre></td></tr></table></figure><p>使用就很简单了，一旦包装好，我们导入它就可以当做官方的组件来使用了，我们可以为他写 State，写 EventHandler，写页面，写交互，写样式，写逻辑，写一切。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这里我们介绍了如何将 igv.js 包装成 React 组件，然后将 React 组件包装成 Reflex 组件，最后在 Reflex 里面使用。这里只是一个简单的例子，实陿上，我们可以包装任何我们想要的组件，只要我们有足够的耐心和技术。本文的代码在 <a href="https://github.com/panxiaoguang/Reflex-IGV">Github</a> 可以获取。</p><div class="tag-plugin colorful note" color="yellow"><div class="title">这里的</div><div class="body"><p>igv.js 是一个简单的例子，实陿上，我们可以包装任何我们想要的组件，只要我们有足够的耐心和技术。</p></div></div>]]></content>
    
    
    <summary type="html">&lt;div class=&quot;tag-plugin colorful note&quot; color=&quot;blue&quot;&gt;&lt;div class=&quot;title&quot;&gt;Reflex&lt;/div&gt;&lt;div class=&quot;body&quot;&gt;&lt;p&gt;是一个开源库，旨在使用纯 Python 构建全栈 Web 应用程序。它的设计理念是让开发者能够完全使用 Python 编写应用的前端和后端，无需学习 JavaScript。这对于那些希望专注于 Python 编程语言的开发者来说是一个重大的福音。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上面说的都是场面话，这都依托于它自身包装的第三方应用够丰富。但是，对于一些冷门的应用，比如我这里要说的 IGV 浏览器，就没有那么容易了。这里我就来分享一下我是如何在 Reflex 里面使用 IGV 浏览器的。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
  </entry>
  
  <entry>
    <title>使用二代和三代WGS数据进行基因组组装</title>
    <link href="https://blog.xiaohanys.top/sec-third-16s-assembly/"/>
    <id>https://blog.xiaohanys.top/sec-third-16s-assembly/</id>
    <published>2024-09-20T05:12:14.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p>本教程中，我们的目标是使用第二代和第三代全基因组测序 (WGS) 数据组装细菌基因组。我们将以此为例来探讨 WGS 数据分析，并探讨测序技术之间的差异。</p><span id="more"></span><h3 id="软件安装"><a href="#软件安装" class="headerlink" title="软件安装"></a>软件安装</h3><h4 id="Docker-镜像"><a href="#Docker-镜像" class="headerlink" title="Docker 镜像"></a>Docker 镜像</h4><p>您可以使用以下命令从<a href="https://hub.docker.com/r/yanhui09/mac2023_extra">DockerHub</a>中提取镜像：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull yanhui09/mac2023_extra</span><br></pre></td></tr></table></figure><h4 id="用mamba安装"><a href="#用mamba安装" class="headerlink" title="用mamba安装"></a>用<code>mamba</code>安装</h4><p>建议使用<code>mamba</code>在<strong>独立</strong> <code>conda</code>环境中安装软件。</p><p>假设您已经在系统中安装了<code>mamba</code>软件，您可以使用<code>mamba</code>创建一个新<code>conda</code>环境：</p><p>转到下载的数据目录。<em>确保您知道自己当前的工作目录位置。</em></p><p>例如，下载的数据目录位于<code>/home/username/MAC2023-extera</code></p><figure class="highlight bash"><table><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"><span class="built_in">cd</span> /home/username/MAC2023-extera</span><br><span class="line"><span class="built_in">pwd</span></span><br></pre></td></tr></table></figure><p>您将看到下载的数据目录的路径，如下所示。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/username/MAC2023-extera</span><br></pre></td></tr></table></figure><p>现在您可以使用以下命令创建新<code>conda</code>环境并安装软件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mamba <span class="built_in">env</span> create -n wgs1 -f envs/env1.yaml</span><br></pre></td></tr></table></figure><p>激活环境以进行以下分析。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">conda activate wgs1</span><br></pre></td></tr></table></figure><h3 id="使用演示数据进行探索"><a href="#使用演示数据进行探索" class="headerlink" title="使用演示数据进行探索"></a>使用演示数据进行探索</h3><p>WGS 数据可以像以前一样从<a href="https://github.com/yanhui09/MAC2023-extra">MAC2023-extra</a>获取。</p><h4 id="首先用seqkit来查看数据"><a href="#首先用seqkit来查看数据" class="headerlink" title="首先用seqkit来查看数据"></a>首先用<code>seqkit</code>来查看数据</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">seqkit <span class="built_in">stat</span> data/wgs/*.fastq.gz data/wgs/ncbi_pacbio_TL110.fasta</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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">file                              format  <span class="built_in">type</span>  num_seqs     sum_len    min_len    avg_len    max_len</span><br><span class="line">data/wgs/NXT20x_R1.fastq.gz       FASTQ   DNA    200,000  30,200,000        151        151        151</span><br><span class="line">data/wgs/NXT20x_R2.fastq.gz       FASTQ   DNA    200,000  30,200,000        151        151        151</span><br><span class="line">data/wgs/ont_r10_20x.fastq.gz     FASTQ   DNA      7,862  51,201,670        129    6,512.6     87,688</span><br><span class="line">data/wgs/ncbi_pacbio_TL110.fasta  FASTA   DNA          1   2,566,312  2,566,312  2,566,312  2,566,312</span><br></pre></td></tr></table></figure><p>这是来自费氏丙酸杆菌菌株的一组测序数据。我们对 Illumina 和 Oxford Nanopore Technologies (ONT) 读数的测序数据进行二次采样，覆盖率达到 20 倍。PacBio 参考基因组来自<a href="https://www.ncbi.nlm.nih.gov/nuccore/NZ_CP085641.1">NCBI RefSeq 数据库</a>。</p><blockquote><p>Q1：该菌株的基因组大小是多少？测序覆盖度是如何计算的？</p><p>illumina：30,200,000*2&#x2F;2,566,312≈20</p><p>ONT：51,201,670&#x2F;2,566,312≈20</p></blockquote><p>我们用<code>fastqc</code>来看看测序数据的质量。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p fastqc/illumina fastqc/ont_r10</span><br><span class="line">fastqc data/wgs/NXT20x_R*.fastq.gz -o ./fastqc/illumina</span><br><span class="line">fastqc data/wgs/ont_r10_20x.fastq.gz -o ./fastqc/ont_r10</span><br></pre></td></tr></table></figure><p>您可以打开<code>.html</code>该<code>fastqc</code>目录下的文件来查看测序数据的质量。</p><p><strong>ONT</strong></p><p><img src="https://picshack.net/ib/4RmGnArPd8.png"></p><p><strong>Illumina</strong></p><p><img src="https://picshack.net/ib/WdQeuUuTKy.png"></p><blockquote><p>我们可以看到 ONT 读取比 Illumina 读取更长，但包含更多错误。</p></blockquote><h4 id="使用-Illumina-数据进行基因组组装"><a href="#使用-Illumina-数据进行基因组组装" class="headerlink" title="使用 Illumina 数据进行基因组组装"></a>使用 Illumina 数据进行基因组组装</h4><h5 id="trimmomatic去除接头"><a href="#trimmomatic去除接头" class="headerlink" title="trimmomatic去除接头"></a><code>trimmomatic</code>去除接头</h5><p><code>trimmomatic</code>是一个用于移除接头和低质量数据的工具。<a href="http://www.usadellab.org/cms/?page=trimmomatic">[阅读更多]</a></p><p>使用<a href="https://emea.illumina.com/products/by-type/sequencing-kits/library-prep-kits/nextera-xt-dna.html">Nextera 文库制备试剂盒从</a><a href="https://emea.illumina.com/systems/sequencing-platforms/nextseq.html">NextSeq</a>平台收集 Illumina 读数。</p><figure class="highlight bash"><table><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"><span class="built_in">mkdir</span> illumina</span><br><span class="line">trimmomatic PE -threads 4 -phred33 data/wgs/NXT20x_R1.fastq.gz data/wgs/NXT20x_R2.fastq.gz illumina/NXT20x_R1_paired.fastq.gz illumina/NXT20x_R1_unpaired.fastq.gz illumina/NXT20x_R2_paired.fastq.gz illumina/NXT20x_R2_unpaired.fastq.gz ILLUMINACLIP:data/wgs/NexteraPE-PE.fa:2:30:10 LEADING:3 TRAILING:3 MINLEN:50</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line">illumina/NXT20x_R1_paired.fastq.gz</span><br><span class="line">illumina/NXT20x_R1_unpaired.fastq.gz</span><br><span class="line">illumina/NXT20x_R2_paired.fastq.gz</span><br><span class="line">illumina/NXT20x_R2_unpaired.fastq.gz</span><br></pre></td></tr></table></figure><h4 id="数据质量控制bbmap"><a href="#数据质量控制bbmap" class="headerlink" title="数据质量控制bbmap"></a>数据质量控制<code>bbmap</code></h4><p><code>bbmap</code>是一套用于测序读数质量控制的工具。它可用于删除冗余数据和 <em>PhiX</em> 对照。<a href="https://jgi.doe.gov/data-and-tools/software-tools/bbtools/bb-tools-user-guide/bbmap-guide/">[阅读更多]</a></p><p><code>clumpify.sh</code>是一个删除冗余数据的工具。<a href="https://jgi.doe.gov/data-and-tools/software-tools/bbtools/bb-tools-user-guide/clumpify-guide/">[阅读更多]</a></p><p><code>bbduk.sh</code>是一种去除污染数据的工具（例如，宿主基因组、<em>PhiX</em> 对照）。<a href="https://jgi.doe.gov/data-and-tools/software-tools/bbtools/bb-tools-user-guide/bbduk-guide/">[阅读更多]</a></p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">#clumpify</span></span><br><span class="line">clumpify.sh <span class="keyword">in</span>=illumina/NXT20x_R1_paired.fastq.gz in2=illumina/NXT20x_R2_paired.fastq.gz out=illumina/NXT20x_R1_paired_dedup.fastq.gz out2=illumina/NXT20x_R2_paired_dedup.fastq.gz dedupe optical spany adjacent</span><br><span class="line"><span class="comment"># bbduk</span></span><br><span class="line">bbduk.sh <span class="keyword">in</span>=illumina/NXT20x_R1_paired_dedup.fastq.gz in2=illumina/NXT20x_R2_paired_dedup.fastq.gz out=illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz out2=illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz ref=data/wgs/phiX174.fasta k=31 hdist=1</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line">illumina/NXT20x_R1_paired_dedup.fastq.gz</span><br><span class="line">illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz</span><br><span class="line">illumina/NXT20x_R2_paired_dedup.fastq.gz</span><br><span class="line">illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz</span><br></pre></td></tr></table></figure><h5 id="基因组组装spades"><a href="#基因组组装spades" class="headerlink" title="基因组组装spades"></a>基因组组装<code>spades</code></h5><p><code>spades</code>是一个用于短读长的基因组组装器。<a href="https://github.com/ablab/spades">[阅读更多]</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">spades.py --isolate -t 4 -1 illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz -2 illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz -o illumina/spades</span><br></pre></td></tr></table></figure><p>预计组装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">illumina/spades/contigs.fasta</span><br></pre></td></tr></table></figure><blockquote><p><em>Q2：我们在这里从分离株中组装了细菌基因组。如果我们有宏基因组样本怎么办？</em></p><p>我们可以使用<code>--meta</code>选项在<code>spades</code>中来组装宏基因组样本。<a href="https://github.com/ablab/spades#basic-options">[阅读更多]</a></p></blockquote><h4 id="使用-ONT-数据进行基因组组装"><a href="#使用-ONT-数据进行基因组组装" class="headerlink" title="使用 ONT 数据进行基因组组装"></a>使用 ONT 数据进行基因组组装</h4><h5 id="使用guppy（可选）或者porechop移除接头"><a href="#使用guppy（可选）或者porechop移除接头" class="headerlink" title="使用guppy（可选）或者porechop移除接头"></a>使用<code>guppy</code>（可选）或者<code>porechop</code>移除接头</h5><blockquote><p>本练习中未提供<code>guppy</code>和<code>porechop</code>的安装如果您想使用它们，请尝试自行安装。</p></blockquote><p><code>guppy</code>是一种用于 ONT 数据的碱基识别和接头修剪的工具。<code>guppy</code>不是开源的，因此需要注册 ONT 帐户才能查看其文档和下载。<a href="https://id.customers.nanoporetech.com/app/nanoporetech-customers_myaccount_1/exk2kkmfwpBAaT3WI697/sso/saml?RelayState=https://community.nanoporetech.com/downloads">[阅读更多]</a></p><p><code>porechop</code>是一个开源工具，用于 ONT 数据的接头修剪。<a href="https://github.com/rrwick/Porechop">[阅读更多]</a></p><p>默认情况下，条形码将在<code>guppy</code>多路分解步骤中被修剪。我们不会对我们的数据重复进行条形码修剪。</p><p>如果您想修剪条形码，可以使用以下命令<code>guppy</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">guppy_barcoder -i data/wgs/ont_r10_20x.fastq.gz -s ont_r10/ont_r10_20x_barcoded.fastq.gz --barcode_kits EXP-NBD104 --trim_barcodes</span><br></pre></td></tr></table></figure><p>以及以下命令<code>porechop</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">porechop -i data/wgs/ont_r10_20x.fastq.gz -o ont_r10/ont_r10_20x_porechop.fastq.gz --threads 4</span><br></pre></td></tr></table></figure><h5 id="seqkit读取质量控制"><a href="#seqkit读取质量控制" class="headerlink" title="seqkit读取质量控制"></a><code>seqkit</code>读取质量控制</h5><p><code>seqkit</code>是一种用于操作测序数据的工具。<a href="https://bioinf.shenwei.me/seqkit/">[阅读更多]</a>这里我们用来<code>seqkit</code>去除短读和低质量数据。</p><figure class="highlight bash"><table><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"><span class="built_in">mkdir</span> ont_r10</span><br><span class="line">seqkit <span class="built_in">seq</span> -j 4 -Q 10 -m 2000 -i data/wgs/ont_r10_20x.fastq.gz -o ont_r10/ont_r10_20x_f.fastq.gz</span><br></pre></td></tr></table></figure><p>使用<code>seqkit stat</code>来检查质量控制之前和之后的 ONT 读数。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">seqkit <span class="built_in">stat</span> data/wgs/ont_r10_20x.fastq.gz ont_r10/ont_r10_20x_f.fastq.gz</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">file                            format  <span class="built_in">type</span>  num_seqs     sum_len  min_len  avg_len  max_len</span><br><span class="line">data/wgs/ont_r10_20x.fastq.gz   FASTQ   DNA      7,862  51,201,670      129  6,512.6   87,688</span><br><span class="line">ont_r10/ont_r10_20x_f.fastq.gz  FASTQ   DNA      6,561  48,742,515    2,001  7,429.1   87,688</span><br></pre></td></tr></table></figure><h5 id="flye基因组组装"><a href="#flye基因组组装" class="headerlink" title="flye基因组组装"></a><code>flye</code>基因组组装</h5><p><code>flye</code>是 ONT 推荐的长读基因组组装器。<a href="https://github.com/fenderglass/Flye">[阅读更多]</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flye --nano-raw ont_r10/ont_r10_20x_f.fastq.gz --out-dir ont_r10/flye --threads 4</span><br></pre></td></tr></table></figure><p>预期的组装输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ont_r10/flye/assembly.fasta</span><br></pre></td></tr></table></figure><blockquote><p><em>Q3：默认</em><code>flye</code>用于组装基因组。如果我们有宏基因组样本怎么办？</p><p>我们可以使用<code>--meta</code>选项来在<code>flye</code>中组装宏基因组样本。<a href="https://github.com/fenderglass/Flye/blob/flye/docs/USAGE.md#-quick-usage">[阅读更多]</a></p></blockquote><h5 id="使用racon与medaka进行基因组矫正"><a href="#使用racon与medaka进行基因组矫正" class="headerlink" title="使用racon与medaka进行基因组矫正"></a>使用<code>racon</code>与<code>medaka</code>进行基因组矫正</h5><p>长读长基因组组装程序通常会产生连续性高但准确性低的基因组草图。需要额外的矫正步骤来提高基因组草图的准确性。但最佳的矫正策略和工具仍在争论中。</p><p><code>racon</code>是一种基于图的相似度算法，用于完善长读长基因组组装。<a href="https://github.com/isovic/racon">[阅读更多]</a>。</p><p><code>medaka</code>是官方为 ONT 数据打造的基于神经网络的数据矫正工具。<a href="https://github.com/nanoporetech/medaka">[阅读更多]</a></p><p>最近，ONT 推荐用<code>medaka</code>直接矫正<code>flye</code>组装后的数据。但结合<code>racon</code>和<code>medaka</code>仍然是一种常见的做法。</p><p><strong>这里我们以</strong><code>medaka</code><strong>在例子：</strong></p><p>由于<code>medaka</code>是基于神经网络的，选择合适的模型会影响抛光效果。您可以用来<code>medaka tools list_models</code>列出所有可用的模型。对于我们的示例，ONT 读数是从<a href="https://store.nanoporetech.com/eu/flow-cell-r10-4-1.html">R10.4.1 flowcell</a>收集的，并且<code>guppy</code>采用<code>hac</code>模式来做碱基生成。</p><figure class="highlight bash"><table><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">medaka tools list_models</span><br><span class="line">medaka_consensus -i ont_r10/ont_r10_20x_f.fastq.gz -d ont_r10/flye/assembly.fasta -o ont_r10/medaka -t 4 -m r1041_e82_260bps_hac_v4.1.0</span><br></pre></td></tr></table></figure><p>预期结果：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ont_r10/medaka/consensus.fasta</span><br></pre></td></tr></table></figure><p><code>racon</code>和<code>medaka</code>混合模式</p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> ont_r10/racon</span><br><span class="line">minimap2 -t 4 -x map-ont ont_r10/flye/assembly.fasta ont_r10/ont_r10_20x_f.fastq.gz &gt; ont_r10/racon/flye_assembly.paf</span><br><span class="line">racon -t 4 ont_r10/ont_r10_20x_f.fastq.gz ont_r10/racon/flye_assembly.paf ont_r10/flye/assembly.fasta &gt; ont_r10/racon/racon.fasta</span><br><span class="line">medaka_consensus -i ont_r10/ont_r10_20x_f.fastq.gz -d ont_r10/racon/racon.fasta -o ont_r10/racon/medaka -t 4 -m r1041_e82_260bps_hac_v4.1.0</span><br></pre></td></tr></table></figure><p>预期的结果：</p><figure class="highlight bash"><table><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">ont_r10/racon/racon.fasta</span><br><span class="line">ont_r10/racon/medaka/consensus.fasta</span><br></pre></td></tr></table></figure><h4 id="ONT-和-Illumina-读取的混合组装"><a href="#ONT-和-Illumina-读取的混合组装" class="headerlink" title="ONT 和 Illumina 读取的混合组装"></a>ONT 和 Illumina 读取的混合组装</h4><p>混合组装是结合不同测序技术优势的常见做法。这里我们选择两种常用的混合组装策略：</p><ol><li><code>pilon</code>：直接使用 ONT 组装作为主干，并用 Illumina 读取对其进行矫正。</li><li><code>unicycler</code>：短读优先混合组装。</li></ol><h5 id="pilonILLUMINA-读取矫正"><a href="#pilonILLUMINA-读取矫正" class="headerlink" title="pilonILLUMINA 读取矫正"></a><code>pilon</code>ILLUMINA 读取矫正</h5><p><code>pilon</code>是一种用短读长优化基因组组装的工具。<a href="https://github.com/broadinstitute/pilon">[阅读更多]</a></p><figure class="highlight bash"><table><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="built_in">mkdir</span> -p hybrid/pilon</span><br><span class="line">bwa index ont_r10/medaka/consensus.fasta</span><br><span class="line">bwa mem -t 4 ont_r10/medaka/consensus.fasta illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz | samtools <span class="built_in">sort</span> -@ 4 -o hybrid/pilon/aligned.bam</span><br><span class="line">samtools index hybrid/pilon/aligned.bam</span><br><span class="line">pilon --genome ont_r10/medaka/consensus.fasta --frags hybrid/pilon/aligned.bam --output hybrid/pilon/pilon --threads 4</span><br></pre></td></tr></table></figure><p>预期产出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hybrid/pilon/pilon.fasta</span><br></pre></td></tr></table></figure><h5 id="可选：unicycler混合组装"><a href="#可选：unicycler混合组装" class="headerlink" title="可选：unicycler混合组装"></a>可选：<code>unicycler</code>混合组装</h5><p><code>unicycler</code>可以进行短读优先混合组装。<a href="https://github.com/rrwick/Unicycler">[阅读更多]</a></p><p><strong>这需要一些时间，因此我们将在示例中跳过这一步。完成其他步骤后，请随时返回此步骤。</strong></p><p><strong>Linux 用户</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">unicycler -l ont_r10/ont_r10_20x_f.fastq.gz -1 illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz -2 illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz -o hybrid/unicycler --threads 4</span><br></pre></td></tr></table></figure><p><strong>MacOS 用户</strong></p><p>由于 on 的最新<code>MacOS</code>版本落后于<a href="https://github.com/rrwick/Unicycler/releases/tag/v0.5.0">0.5.0</a>，我们附加两个标志和来保持最大的兼容性。<code>unicyclerconda--no_correct--no_pilon</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">unicycler --no_correct --no_pilon -l ont_r10/ont_r10_20x_f.fastq.gz -1 illumina/NXT20x_R1_paired_dedup_deduk.fastq.gz -2 illumina/NXT20x_R2_paired_dedup_deduk.fastq.gz -o hybrid/unicycler</span><br></pre></td></tr></table></figure><p>预期的产出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hybrid/unicycler/assembly.fasta</span><br></pre></td></tr></table></figure><h4 id="使用quast对组装基因组进行质量评估"><a href="#使用quast对组装基因组进行质量评估" class="headerlink" title="使用quast对组装基因组进行质量评估"></a>使用<code>quast对</code>组装基因组进行质量评估</h4><p>我们已经生成了许多具有不同策略的程序集。让我们使用基于 PacBio 深度测序的完整组装作为参考基因组来评估每个数据集的质量。如果您尚未完成所有组装步骤，您可以使用<code>./data/wgs/assemblies</code>提供的组装文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">seqkit <span class="built_in">stat</span> data/wgs/assemblies/*.fasta data/wgs/ncbi_pacbio_TL110.fasta</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line">file                                               format  <span class="built_in">type</span>  num_seqs    sum_len    min_len      avg_len    max_len</span><br><span class="line">data/wgs/assemblies/hybrid_pilon.fasta             FASTA   DNA          2  2,579,926     29,242    1,289,963  2,550,684</span><br><span class="line">data/wgs/assemblies/hybrid_pilon_proovframe.fasta  FASTA   DNA          2  2,579,979     29,245  1,289,989.5  2,550,734</span><br><span class="line">data/wgs/assemblies/hybrid_unicycler.fasta         FASTA   DNA          1  2,564,177  2,564,177    2,564,177  2,564,177</span><br><span class="line">data/wgs/assemblies/illumina.fasta                 FASTA   DNA        217  2,527,918         78     11,649.4     60,978</span><br><span class="line">data/wgs/assemblies/ont_flye.fasta                 FASTA   DNA          2  2,579,333     29,239  1,289,666.5  2,550,094</span><br><span class="line">data/wgs/assemblies/ont_flye_medaka.fasta          FASTA   DNA          2  2,579,052     29,238    1,289,526  2,549,814</span><br><span class="line">data/wgs/assemblies/ont_flye_racon.fasta           FASTA   DNA          2  2,578,932     29,231    1,289,466  2,549,701</span><br><span class="line">data/wgs/assemblies/ont_flye_racon_medaka.fasta    FASTA   DNA          2  2,578,566     29,229    1,289,283  2,549,337</span><br><span class="line">data/wgs/ncbi_pacbio_TL110.fasta                   FASTA   DNA          1  2,566,312  2,566,312    2,566,312  2,566,312</span><br></pre></td></tr></table></figure><blockquote><p>与参考相比，各个组装的总长度相似。</p><p>但序列数 ( <code>num_seqs</code>) 和最大序列长度 ( <code>max_len</code>) 变化很大。</p></blockquote><p>让我们用<code>quast</code>来检查各个组装的质量。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">quast data/wgs/assemblies/*.fasta -r data/wgs/ncbi_pacbio_TL110.fasta -o quast</span><br></pre></td></tr></table></figure><p>您可以打开<code>quast</code>目录中的<code>report.html</code>文件来查看各个组装数据集的质量。</p><p><img src="https://picshack.net/ib/d6khde5lGw.png"></p><blockquote><p><em>Q4：根据示例，您认为哪种测序技术在基因组完整性和连续性方面效果更好？</em></p><p>检查<code>Genome fraction</code>和<code>NGA50</code></p><p><em>Q5：根据示例，您认为哪种装配的精度最好？</em></p><p>检查<code>Misassemblies</code>和<code>Mismatches</code></p><p><em>Q6：根据示例，ONT 组件中的主要错误类型是什么？</em></p><p>检查<code>mismatches</code>和<code>indels</code>。<a href="https://genome.sph.umich.edu/wiki/Indel">[阅读更多]</a></p><p><em>Q7：您认为此示例的最佳组装策略是什么？</em></p></blockquote><h4 id="可选：参考引导校正proovframe"><a href="#可选：参考引导校正proovframe" class="headerlink" title="可选：参考引导校正proovframe"></a>可选：参考引导校正<code>proovframe</code></h4><p>高频率<code>indels</code>会导致 ONT 组装的编码序列 (CDS) 发生移码。通过参考引导校正，我们可以校正 ONT 组装的 CDS 中的移码。</p><p><strong>此步骤是可选的，如果您没有时间，可以跳过它。</strong></p><p>为了使用该工具，由于依赖冲突，<code>proovframe</code>我们需要创建另一个<code>conda</code>环境。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">conda deactivate</span><br><span class="line">mamba <span class="built_in">env</span> create -n wgs2 -f envs/env2.yaml</span><br><span class="line">conda activate wgs2</span><br></pre></td></tr></table></figure><h5 id="基因组注释prokka"><a href="#基因组注释prokka" class="headerlink" title="基因组注释prokka"></a>基因组注释<code>prokka</code></h5><p><code>prokka</code>是快速注释细菌基因组的常用工具。<a href="https://github.com/tseemann/prokka">[阅读更多]</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> proovframe</span><br><span class="line">conda activate wgs2</span><br><span class="line">prokka --outdir proovframe/prokka --prefix pacbio --cpus 4 data/wgs/ncbi_pacbio_TL110.fasta</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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">proovframe/prokka/pacbio.err</span><br><span class="line">proovframe/prokka/pacbio.faa</span><br><span class="line">proovframe/prokka/pacbio.ffn</span><br><span class="line">proovframe/prokka/pacbio.fna</span><br><span class="line">proovframe/prokka/pacbio.fsa</span><br><span class="line">proovframe/prokka/pacbio.gbk</span><br><span class="line">proovframe/prokka/pacbio.gff</span><br><span class="line">proovframe/prokka/pacbio.log</span><br><span class="line">proovframe/prokka/pacbio.sqn</span><br><span class="line">proovframe/prokka/pacbio.tbl</span><br><span class="line">proovframe/prokka/pacbio.tsv</span><br><span class="line">proovframe/prokka/pacbio.txt</span><br></pre></td></tr></table></figure><p>我们有许多来自<code>prokka</code>的输出文件。这里我们仅使用翻译后的蛋白质序列（<code>./proovframe/prokka/pacbio.faa</code>文件）</p><h5 id="移码校正proovframe"><a href="#移码校正proovframe" class="headerlink" title="移码校正proovframe"></a>移码校正<code>proovframe</code></h5><p><img src="https://picshack.net/ib/Z55fiIPp1W.png"></p><p><code>proovframe</code>是 CDS 中基于参考序列的移码校正工具。<a href="https://www.biorxiv.org/content/10.1101/2021.08.23.457338v1">[阅读更多]</a></p><figure class="highlight bash"><table><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">proovframe map -a proovframe/prokka/pacbio.faa -o proovframe/pilon.tsv hybrid/pilon/pilon.fasta</span><br><span class="line">proovframe fix -o proovframe/pilon_corrected.fasta hybrid/pilon/pilon.fasta proovframe/pilon.tsv</span><br></pre></td></tr></table></figure><p>预期的输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proovframe/pilon_corrected.fasta</span><br></pre></td></tr></table></figure><blockquote><p><em>Q8：在之前的报告中，</em><code>shybrid_pilon_proovframe.fasta</code><em>中的</em><code>N&#39;</code><em>会增加。你觉得这是好是坏？</em></p></blockquote><blockquote><p>本文为学习记录，课程作者为：<a href="https://www.yanh.org/">YanHui</a></p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;本教程中，我们的目标是使用第二代和第三代全基因组测序 (WGS) 数据组装细菌基因组。我们将以此为例来探讨 WGS 数据分析，并探讨测序技术之间的差异。&lt;/p&gt;</summary>
    
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/categories/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/tags/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
  </entry>
  
  <entry>
    <title>使用NART通过读取分类进行长扩增子分析</title>
    <link href="https://blog.xiaohanys.top/nart-16s-long/"/>
    <id>https://blog.xiaohanys.top/nart-16s-long/</id>
    <published>2024-09-20T05:11:35.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<p><code>NART</code>设计用于基于图谱的纳米孔扩增子（<strong>实时</strong>）分析，例如 16S rRNA 基因。<code>NART</code>由<code>NART</code>（Nanopore Amplicon Real-Time entry）和 <code>NAWF</code>（Nanopore Amplicon <code>snakemake</code> WorkFlow entry）组成。通过基于映射的策略提供从基础调用读取到最终计数矩阵的（实时）端到端解决方案。</p><span id="more"></span><p><code>nawf</code>提供三个选项（<code>emu</code>，<code>minimap2lca</code>和<code>blast2lca</code>）来确定微生物组成。</p><p><img src="https://picshack.net/ib/O8BXIaIwax.png"></p><h3 id="NART-安装"><a href="#NART-安装" class="headerlink" title="NART 安装"></a>NART 安装</h3><p>完整的安装指南<code>NART</code>可<a href="https://github.com/yanhui09/nart">在此处</a>获取。</p><p>您可以根据您的喜好选择<code>NART</code>使用<code>docker</code>映像安装（这只是<code>MacOS</code>用户的解决方案）或从<code>GitHub</code>存储库安装。</p><h4 id="Docker-镜像"><a href="#Docker-镜像" class="headerlink" title="Docker 镜像"></a>Docker 镜像</h4><p>最简单的使用方法是从<a href="https://hub.docker.com/r/yanhui09/nart">Docker Hub</a>拉取<code>NART</code>镜像以获得跨平台支持。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull yanhui09/nart</span><br></pre></td></tr></table></figure><blockquote><p><code>NART</code>是通过<code>docker</code>为<code>linux/amd64</code>平台而构建的，</p><p><code>MacOS</code>用户需要使用 docker 容器来运行<code>NART</code>。</p></blockquote><h4 id="从-GitHub-存储库安装"><a href="#从-GitHub-存储库安装" class="headerlink" title="从 GitHub 存储库安装"></a>从 GitHub 存储库安装</h4><p><strong>1.克隆 Github 仓库并创建隔离<code>conda</code>环境</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/yanhui09/nart.git</span><br><span class="line"><span class="built_in">cd</span> nart</span><br><span class="line">mamba <span class="built_in">env</span> create -n nart -f env.yaml</span><br></pre></td></tr></table></figure><p><strong>2.安装<code>NART</code></strong></p><p>为了避免不一致，建议<code>NART</code>在上述<code>conda</code>环境中安装</p><figure class="highlight bash"><table><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">conda activate nart</span><br><span class="line">pip install --editable .</span><br></pre></td></tr></table></figure><h3 id="使用-NART-运行的演示"><a href="#使用-NART-运行的演示" class="headerlink" title="使用 NART 运行的演示"></a>使用 NART 运行的演示</h3><p><a href="https://github.com/yanhui09/nart#usage">在这里</a>找到完整的使用指南。</p><p><a href="https://www.youtube.com/watch?v=TkdJGLOscPg">可以在此处</a>找到视频教程。</p><h4 id="快速启动示例"><a href="#快速启动示例" class="headerlink" title="快速启动示例"></a>快速启动示例</h4><h5 id="单批次扩增子分析"><a href="#单批次扩增子分析" class="headerlink" title="单批次扩增子分析"></a>单批次扩增子分析</h5><p><code>nawf</code>可用于分析 Nanopore 运行或批次中的任何单个碱基调用文件。</p><figure class="highlight bash"><table><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">nawf config -b /path/to/basecall_fastq -d /path/to/database    <span class="comment"># init config file and check</span></span><br><span class="line">nawf run all                                                   <span class="comment"># start analysis</span></span><br></pre></td></tr></table></figure><h5 id="实时分析"><a href="#实时分析" class="headerlink" title="实时分析"></a>实时分析</h5><p><code>nart</code>提供实用程序来记录、处理和分析连续生成的<code>fastq</code>批次。</p><p>在开始实时分析之前，您需要<code>nawf</code>根据需要配置工作流程。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nawf config -d /path/to/database                               <span class="comment"># init config file and check</span></span><br></pre></td></tr></table></figure><p>在常见情况下，您需要三个独立的会话来分别处理监控、处理和可视化。</p><p><strong>1.监听 bascall 输出并记录</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart monitor -q /path/to/basecall_fastq_dir                    <span class="comment"># monitor basecall output</span></span><br></pre></td></tr></table></figure><p><strong>2.开始新的 fastq 的扩增子分析</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart run -t 10                                                 <span class="comment"># real-time process in batches</span></span><br></pre></td></tr></table></figure><p><strong>3.更新特征表以在浏览器中交互式可视化</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart visual                                                    <span class="comment"># interactive visualization</span></span><br></pre></td></tr></table></figure><h4 id="熟悉NART使用"><a href="#熟悉NART使用" class="headerlink" title="熟悉NART使用"></a>熟悉<code>NART</code>使用</h4><p><code>NART</code>由两组脚本组成：<code>nart</code>和<code>nawf</code>，分别控制实时分析和工作流性能。</p><p>如果<code>NART</code>安装在<code>conda</code>环境中，请记住激活环境<code>conda</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">conda activate nart</span><br><span class="line">nawf -h</span><br><span class="line">nart -h</span><br></pre></td></tr></table></figure><p><strong>要使用 docker 镜像</strong>，您需要将数据目录（例如<code>pwd</code> ）挂载到容器中<code>/home目录</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker run -it -v `<span class="built_in">pwd</span>`:/home --network host --privileged yanhui09/nart</span><br><span class="line">nawf -h</span><br><span class="line">nart -h</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意：</strong><code>--network host</code>需要<code>nart monitor</code>才能正常工作。</p></blockquote><h4 id="使用演示数据集运行NART"><a href="#使用演示数据集运行NART" class="headerlink" title="使用演示数据集运行NART"></a>使用演示数据集运行<code>NART</code></h4><p><strong>0.确保您已从<a href="https://github.com/yanhui09/MAC2023-extra">此处</a>下载所需的演示数据集。然后进入目录<code>cd</code>。</strong></p><p>例如，输入绝对路径（“长路径”）的目录是<code>/home/me/MAC2023-extra</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /home/me/MAC2023-extra</span><br></pre></td></tr></table></figure><p>如果您尚未下载使用<code>Git</code>下载数据，</p><figure class="highlight bash"><table><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">git <span class="built_in">clone</span> https://github.com/yanhui09/MAC2023-extra.git</span><br><span class="line"><span class="built_in">cd</span> ./MAC2023-extra</span><br></pre></td></tr></table></figure><h4 id="nawf分析已完成的-ONT"><a href="#nawf分析已完成的-ONT" class="headerlink" title="nawf分析已完成的 ONT"></a><code>nawf</code>分析已完成的 ONT</h4><p><strong>1.1. 检查您所在的位置并尝试<code>laca init</code>检查生成的<code>config.yaml</code>文件。</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">pwd</span></span><br><span class="line">nawf config -b ./data/ont16s/*.fastq.gz -d ./database -w ./nart_output</span><br><span class="line"><span class="built_in">cat</span> ./nart_output/config.yaml</span><br></pre></td></tr></table></figure><p><strong>1.2.开始<code>nawf</code>试运行</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nawf run all -w ./nart_output -n</span><br></pre></td></tr></table></figure><h5 id="2-实时分析nart"><a href="#2-实时分析nart" class="headerlink" title="2. 实时分析nart"></a>2. 实时分析<code>nart</code></h5><p><strong>2.1.</strong> 重新生成<code>config.yaml</code>不带<code>-b</code>标志的文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf ./nart_output</span><br><span class="line">nawf config -d ./database -w ./nart_output</span><br><span class="line"><span class="built_in">head</span> ./nart_output/config.yaml</span><br></pre></td></tr></table></figure><blockquote><p><code>basecall_fq</code>检查文件中的更改<code>config.yaml</code>。</p></blockquote><p><strong>2.2.</strong> 监控 bascalling 输出并记录</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart monitor -q ./data/ont16s -w ./nart_output</span><br></pre></td></tr></table></figure><p><code>nart monitor</code>在工作目录中创建一个<code>fqs.txt</code>来记录即将到来的<code>fastq</code>文件。</p><p><strong>2.3.</strong> 开始扩增子分析（需要新终端）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart run -t 4 -w ./nart_output</span><br></pre></td></tr></table></figure><blockquote><p>在新终端中，检查以下操作<code>nart monitor</code></p><figure class="highlight bash"><table><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> ./nart_output</span><br><span class="line"><span class="built_in">cat</span> ./nart_output/fqs.txt</span><br><span class="line"><span class="built_in">cp</span> ./data/ont16s/*.fastq.gz ./data/ont16s/new.fastq.gz</span><br><span class="line"><span class="built_in">cat</span> ./nart_output/fqs.txt</span><br></pre></td></tr></table></figure><p>检查文件内容<code>fqs.txt</code>的变化。</p></blockquote><p><code>nart run</code>将特定于批次的计数矩阵存储在文件夹<code>batches</code>下。当创建新矩阵时，合并表 ( <code>otu_table.tsv</code>) 会迭代更新。</p><p>当您在输出文件夹中看到一个 otu_table.tsv，例如<code>nart_output/</code>，您可以尝试下面的交互式可视化。</p><p><strong>2.4.</strong> 浏览器中的交互式可视化（需要新终端）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nart visual -i ./nart_output</span><br></pre></td></tr></table></figure><p>在浏览器中打开生成的链接。您预计会看到如下所示的交互式条形图。<img src="https://picshack.net/ib/ScRCwcE8ur.png"></p><blockquote><p><code>MacOS</code>用户无法通过<code>docker</code>体验<code>nart visual</code>。:(</p><p>主机网络驱动程序仅适用于 Linux 主机，在 Docker Desktop for Mac、Docker Desktop for Windows 或 Docker EE for Windows Server 上不受支持。<a href="https://docs.docker.com/network/drivers/host/">[阅读更多]</a></p></blockquote><p>本文为教程整理，原作者为<a href="https://www.yanh.org/">YanHui</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;code&gt;NART&lt;/code&gt;设计用于基于图谱的纳米孔扩增子（&lt;strong&gt;实时&lt;/strong&gt;）分析，例如 16S rRNA 基因。&lt;code&gt;NART&lt;/code&gt;由&lt;code&gt;NART&lt;/code&gt;（Nanopore Amplicon Real-Time entry）和 &lt;code&gt;NAWF&lt;/code&gt;（Nanopore Amplicon &lt;code&gt;snakemake&lt;/code&gt; WorkFlow entry）组成。通过基于映射的策略提供从基础调用读取到最终计数矩阵的（实时）端到端解决方案。&lt;/p&gt;</summary>
    
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/categories/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/tags/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
  </entry>
  
  <entry>
    <title>使用LACA从长扩增子中从头挑选OTU</title>
    <link href="https://blog.xiaohanys.top/laca-16s-long/"/>
    <id>https://blog.xiaohanys.top/laca-16s-long/</id>
    <published>2024-09-20T05:10:53.000Z</published>
    <updated>2026-03-26T09:21:36.505Z</updated>
    
    <content type="html"><![CDATA[<p><code>LACA</code>是用于长扩增子一致性分析（例如 16S rRNA 基因扩增子分析）的可重复且可扩展的工作流程。它使用用<code>snakemake</code>管理工作流程以及<code>conda来</code>管理环境。</p><span id="more"></span><h3 id="LACA-的安装"><a href="#LACA-的安装" class="headerlink" title="LACA 的安装"></a>LACA 的安装</h3><p>完整的安装指南<code>LACA</code>可<a href="https://github.com/yanhui09/laca">在此处</a>获取。</p><p>您可以根据您的喜好选择使用<code>docker</code>或从<code>GitHub</code>存储库安装<code>LACA</code></p><h4 id="Docker-镜像"><a href="#Docker-镜像" class="headerlink" title="Docker 镜像"></a>Docker 镜像</h4><p>最简单的使用方法是从<a href="https://hub.docker.com/r/yanhui09/laca">Docker Hub</a>拉取<code>LACA</code>镜像以获得跨平台支持</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull yanhui09/laca</span><br></pre></td></tr></table></figure><blockquote><p><code>LACA</code>是通过<code>docker</code>为<code>linux/amd64</code>平台而构建的，</p><p><code>MacOS</code>用户需要使用 docker 容器来运行<code>LACA</code>。</p></blockquote><h4 id="从-GitHub-存储库安装"><a href="#从-GitHub-存储库安装" class="headerlink" title="从 GitHub 存储库安装"></a>从 GitHub 存储库安装</h4><p><strong>1.克隆 Github 仓库并创建隔离<code>conda</code>环境</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/yanhui09/laca.git</span><br><span class="line"><span class="built_in">cd</span> laca</span><br><span class="line">mamba <span class="built_in">env</span> create -n laca -f env.yaml</span><br></pre></td></tr></table></figure><p><strong>2.安装<code>LACA</code></strong></p><p>为了避免不一致，建议在上面建立的<code>conda</code>环境中安装<code>LACA</code></p><figure class="highlight bash"><table><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">conda activate laca</span><br><span class="line">pip install --editable .</span><br></pre></td></tr></table></figure><h3 id="使用-LACA-运行演示数据"><a href="#使用-LACA-运行演示数据" class="headerlink" title="使用 LACA 运行演示数据"></a>使用 LACA 运行演示数据</h3><p><a href="https://github.com/yanhui09/laca#usage">在这里</a>找到完整的使用指南。</p><h4 id="快速启动示例"><a href="#快速启动示例" class="headerlink" title="快速启动示例"></a>快速启动示例</h4><figure class="highlight bash"><table><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">laca init -b /path/to/basecalled_fastqs -d /path/to/database    <span class="comment"># init config file and check</span></span><br><span class="line">laca run all                                         <span class="comment"># start analysis</span></span><br></pre></td></tr></table></figure><h4 id="熟悉LACA使用"><a href="#熟悉LACA使用" class="headerlink" title="熟悉LACA使用"></a>熟悉<code>LACA</code>使用</h4><p><code>LACA</code>很容易使用。您可以使用<code>laca init</code>和<code>laca run</code>分两步开始新的分析。</p><p>如果<code>LACA</code>安装在<code>conda</code>环境中，请记住激活<code>conda</code>环境。</p><figure class="highlight bash"><table><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">conda activate laca</span><br><span class="line">laca -h</span><br></pre></td></tr></table></figure><p><strong>要使用 docker 镜像，您需要将数据目录（例如<code>pwd</code>）挂载到容器中<code>/home</code> 目录。</strong></p><figure class="highlight bash"><table><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">docker run -it -v `<span class="built_in">pwd</span>`:/home --privileged yanhui09/laca</span><br><span class="line">laca -h</span><br></pre></td></tr></table></figure><p><strong>1.初始化配置文件<code>laca init</code></strong></p><p><code>laca init</code>会在工作目录中生成一个配置文件，其中包含运行<code>LACA</code>所需的所有参数。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">laca init -h</span><br></pre></td></tr></table></figure><p><strong>2.开始<code>laca run</code>分析</strong></p><p><code>laca run</code>将相应地触发完整的工作流程或定义资源下的特定模块。使用<code>laca run -h</code>获得试运行概述。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">laca run -h</span><br></pre></td></tr></table></figure><h4 id="使用演示数据集运行LACA"><a href="#使用演示数据集运行LACA" class="headerlink" title="使用演示数据集运行LACA"></a>使用演示数据集运行<code>LACA</code></h4><p><strong>0.确保您已从<a href="https://github.com/yanhui09/MAC2023-extra">此处</a>下载所需的演示数据集。然后<code>cd</code>进入目录。</strong></p><p>例如，输入绝对路径（“长路径”）<code>/home/me/MAC2023-extra</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /home/me/MAC2023-extra</span><br></pre></td></tr></table></figure><p>如果您尚未下载数据用<code>Git</code>下载，</p><figure class="highlight bash"><table><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">git <span class="built_in">clone</span> https://github.com/yanhui09/MAC2023-extra.git</span><br><span class="line"><span class="built_in">cd</span> ./MAC2023-extra</span><br></pre></td></tr></table></figure><p><strong>1.检查您所在的位置并尝试<code>laca init</code>检查生成的<code>config.yaml</code>文件。</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">pwd</span></span><br><span class="line">laca init -b ./data/ont16s -d ./database -w ./laca_output --fqs-min 50</span><br><span class="line"><span class="built_in">cat</span> ./laca_output/config.yaml</span><br></pre></td></tr></table></figure><p><strong>2.<code>LACA</code>伪运行和真实运行</strong></p><figure class="highlight bash"><table><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">laca run all -w ./laca_output -n</span><br><span class="line">laca run kmerCon -j 4 -w ./laca_output</span><br></pre></td></tr></table></figure><p><code>LACA</code>能够生成<code>otu table</code>，<code>taxonomy table</code>以及<code>phylogenetic tree</code>如果您使用<code>laca run all</code>运行完整的工作流程。但第一次使用需要时间准备数据库和安装。</p><p>作为一个例子，这里我们只运行模块<code>kmerCon</code>来根据 kmer 频率提取一致序列。</p><p>看看这些共有序列，取第一个序列对<code>rRNA/ITS</code>数据库进行<a href="https://blast.ncbi.nlm.nih.gov/Blast.cgi">BLAST</a>搜索。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">head</span> -n2 ./laca_output/kmerCon/kmerCon.fna</span><br></pre></td></tr></table></figure><p>预期输出：</p><figure class="highlight bash"><table><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">&gt;pooled_0b000_0cand1</span><br><span class="line">CACAATGGGCGCAAGCCTGATGCAGCGACGCCGCGTGCGGGATGACGGCCTTCGGGTTGTAAACCGCTTTTGACTGGGAGCAAGCCCTTCGGGGTGAGTGTACCTTTCGAATAAGCACCGGCTAACTACGTGCCAGCAGCCGCGGTAATACGTAGGGTGCAAGCGTTATCCGGAATTATTGGGCGTAAAGGGCTCGTAGGCGGTTCGTCGCGTCCGGTGTGAAAGTCCATCGCTTAACGGTGGATCCGCGCCGGGTACGGGCGGGCTTGAGTGCGGTAGGGGAGACTGGAATTCCCGGTGTAACGGTGGAATGTGTAGATATCGGGAAGAACACCAATGGCGAAGGCAGGTCTCTGGGCCGTCACTGACGCTGAGGAGCGAAAGCGTGGGGAGCGAACAGGATTAGATACCCTGGTAGTCCACGCCGTAAACGGTGGATGCTGGATGTGGGGACCATTCCACGGTCTCCGTGTCGGAGCCAACGCGTTAAGCATCCCGCCTGGGGAGTACGGCCGCAAGGCTAAAACTCAAAGAAATTGACGGGGGCCCGCACAAGCGGCGGAGCATGCGGATTAATTCGATGCAACGCGAAGAACCTTACCTGGGCTTGACATGTTCCCGACAGCCGTAGAGATACGGCCTCCCTTCGGGGCGGGTTCACAGGTGGTGCATGGTCGTCGTCAGCTCGTGTCGTGAGATGTTGGGTTAAGTCCCGCAACGAGCGCAACCCTCGCCCTGTGTTGCCAGCACGTCGTGGTGGGAACTCACGGGGGACCGCCGGGGTCAACTCGGAGGAAGGTGGGGATGACGTCAGATCATCATGCCCCTTACGTCCAGGGCTTCACGCATGCTACAATGGCCGGTACAACGGGATGCGACCTCGCGAGGGGGAGCGGATCCCTTAAAACCGGTCTCAGTTCGGATTGGAGTCTGCAACCCGACTCCATGAAGGCGGAGTCGCTAGTAATCGCGGATCAGCAACGCCGCGGTGAATGCGTTCCCGGGCC</span><br></pre></td></tr></table></figure><p><img src="https://picshack.net/ib/ERENmtCCeN.jpg"></p><p>结果<code>BLAST</code>表明，该序列是一个<code>16S rRNA</code>基因片段，<code>Bifidobacterium</code>一性性超过 99%。</p><blockquote><p>读取一个 ONT 并进行相同的<code>BLAST</code>搜索。您希望看到什么？</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zcat ./data/ont16s/*.fastq.gz | <span class="built_in">head</span></span><br></pre></td></tr></table></figure><p>本文为学习记录，课程作者为：<a href="https://www.yanh.org/">YanHui</a></p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;code&gt;LACA&lt;/code&gt;是用于长扩增子一致性分析（例如 16S rRNA 基因扩增子分析）的可重复且可扩展的工作流程。它使用用&lt;code&gt;snakemake&lt;/code&gt;管理工作流程以及&lt;code&gt;conda来&lt;/code&gt;管理环境。&lt;/p&gt;</summary>
    
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/categories/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
    
    <category term="生物信息" scheme="https://blog.xiaohanys.top/tags/%E7%94%9F%E7%89%A9%E4%BF%A1%E6%81%AF/"/>
    
  </entry>
  
  <entry>
    <title>用VueJS和Fastapi通过websocket实现进度条追踪</title>
    <link href="https://blog.xiaohanys.top/vue-fastapi-websocket/"/>
    <id>https://blog.xiaohanys.top/vue-fastapi-websocket/</id>
    <published>2024-09-20T05:09:13.000Z</published>
    <updated>2026-03-26T09:21:36.509Z</updated>
    
    <content type="html"><![CDATA[<h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>在我们的应用小程序中，我们是前后端分离的。前端页面只负责渲染，而后端需要处理数据。但是如果遇到数据量很大的情况下，我们处理起来就很缓慢，如果我们想通过 AJAX 的方法追踪后台数据变化的进度，需要用到轮询的方案，这个是非常消耗资源的。这里我们用 VueJS 和 Fastapi 的小例子演示前端传递数据，后台用 10 秒处理数据并实时反应进度给前台的实现。</p><span id="more"></span><h3 id="后端-Fastapi"><a href="#后端-Fastapi" class="headerlink" title="后端 Fastapi"></a>后端 Fastapi</h3><p>因为后端写起来相对简单，我们先写个后端</p><p>新建<code>my-fastapi</code>目录，然后新建<code>main.py</code>文件，代码如下所示：</p><figure class="highlight python"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> time <span class="keyword">import</span> sleep</span><br><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> FastAPI, WebSocket <span class="comment">##导入websocket</span></span><br><span class="line"><span class="comment">##创建app</span></span><br><span class="line">app = FastAPI()</span><br><span class="line"><span class="comment">##路由端点需要websocket</span></span><br><span class="line"><span class="meta">@app.websocket(<span class="params"><span class="string">&quot;/ws&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">websocket_endpoint</span>(<span class="params">websocket: WebSocket</span>):</span><br><span class="line">    <span class="keyword">await</span> websocket.accept()  <span class="comment">## 等待链接</span></span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        data = <span class="keyword">await</span> websocket.receive_json() <span class="comment">###等待接收Json数据</span></span><br><span class="line">        <span class="comment">### 这是一个10S的任务</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">10</span>):</span><br><span class="line">            sleep(<span class="number">1</span>)</span><br><span class="line">            <span class="comment">### 将此刻的循环次数传递给前端</span></span><br><span class="line">            <span class="keyword">await</span> websocket.send_json(&#123;<span class="string">&quot;data&quot;</span>: <span class="string">&quot;&quot;</span>, <span class="string">&quot;time&quot;</span>: (i+<span class="number">1</span>)*<span class="number">10</span>&#125;)</span><br><span class="line">        jieguo = data[<span class="string">&#x27;input&#x27;</span>] + <span class="string">&quot; finished!&quot;</span></span><br><span class="line">        <span class="comment">### 等待返回数据</span></span><br><span class="line">        <span class="keyword">await</span> websocket.send_json(&#123;<span class="string">&quot;data&quot;</span>: jieguo, <span class="string">&quot;time&quot;</span>: <span class="number">100</span>&#125;)</span><br></pre></td></tr></table></figure><h3 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h3><p>前端用 Vuejs+NaiveUI 实现：</p><p>创建一个 Vite 项目：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm create vite@latest my-vue-app -- --template vue</span><br></pre></td></tr></table></figure><p>安装 Naive-UI</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i -D naive-ui vfonts</span><br></pre></td></tr></table></figure><p>将<code>src/app.vue</code> 中的内容删除，替换成下面的：</p><figure class="highlight plaintext"><table><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line">&lt;script setup&gt;</span><br><span class="line">///UI</span><br><span class="line">import &#123;</span><br><span class="line">  NButton,</span><br><span class="line">  NInput,</span><br><span class="line">  NSpace,</span><br><span class="line">  NH1,</span><br><span class="line">  NProgress,</span><br><span class="line">  useThemeVars,</span><br><span class="line">&#125; from &quot;naive-ui&quot;;</span><br><span class="line">import &#123; onMounted, ref &#125; from &quot;vue&quot;;</span><br><span class="line">import &#123; changeColor &#125; from &quot;seemly&quot;;</span><br><span class="line">/// 绑定数据</span><br><span class="line">const data = ref(&quot;&quot;);</span><br><span class="line">const tm = ref(&quot;&quot;);</span><br><span class="line">const inputData = ref(&quot;&quot;);</span><br><span class="line">/// 建立持久通信</span><br><span class="line">const connection = new WebSocket(&quot;ws://localhost:8000/ws&quot;);</span><br><span class="line">const themeVars = useThemeVars();</span><br><span class="line"></span><br><span class="line">///这里用来向后端发送Json数据，注意要转换为字符串格式 JSON.stringify()</span><br><span class="line">const submit = () =&gt; &#123;</span><br><span class="line">  connection.send(JSON.stringify(&#123; input: inputData.value &#125;));</span><br><span class="line">&#125;;</span><br><span class="line">/// 这里接收数据，注意要把字符串格式的JSON解析为json对象 JSON.parse()</span><br><span class="line">onMounted(() =&gt; &#123;</span><br><span class="line">  connection.onmessage = function (e) &#123;</span><br><span class="line">    const backData = JSON.parse(e.data);</span><br><span class="line">    data.value = backData.data;</span><br><span class="line">    tm.value = backData.time;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;);</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;n-space justify=&quot;center&quot; vertical&gt;</span><br><span class="line">    &lt;n-h1 style=&quot;text-align: center;&quot;&gt;Hello &#123;&#123; data &#125;&#125;&lt;/n-h1&gt;</span><br><span class="line">    &lt;n-space justify=&quot;center&quot;&gt;</span><br><span class="line">      &lt;n-progress</span><br><span class="line">        style=&quot;width: 300px;&quot;</span><br><span class="line">        type=&quot;line&quot;</span><br><span class="line">        indicator-placement=&quot;inside&quot;</span><br><span class="line">        :color=&quot;themeVars.errorColor&quot;</span><br><span class="line">        :rail-color=&quot;changeColor(themeVars.errorColor, &#123; alpha: 0.2 &#125;)&quot;</span><br><span class="line">        :percentage=&quot;tm&quot;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/n-space&gt;</span><br><span class="line">    &lt;n-space style=&quot;display: flex; margin: 10px auto; justify-content: center;&quot;&gt;</span><br><span class="line">      &lt;n-input v-model:value=&quot;inputData&quot; type=&quot;text&quot; placeholder=&quot;测试输入&quot; /&gt;</span><br><span class="line">      &lt;n-button type=&quot;primary&quot; @click=&quot;submit&quot;&gt;Submit&lt;/n-button&gt;</span><br><span class="line">    &lt;/n-space&gt;</span><br><span class="line">  &lt;/n-space&gt;</span><br><span class="line">&lt;/template&gt;</span><br></pre></td></tr></table></figure><p>最终的效果如图所示：</p><p><em>这个动图没有录制完整，最终会到 100%。</em></p><p><img src="https://picshack.net/ib/Psoo0yimK7.gif" alt="showing"></p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;简介&quot;&gt;&lt;a href=&quot;#简介&quot; class=&quot;headerlink&quot; title=&quot;简介&quot;&gt;&lt;/a&gt;简介&lt;/h3&gt;&lt;p&gt;在我们的应用小程序中，我们是前后端分离的。前端页面只负责渲染，而后端需要处理数据。但是如果遇到数据量很大的情况下，我们处理起来就很缓慢，如果我们想通过 AJAX 的方法追踪后台数据变化的进度，需要用到轮询的方案，这个是非常消耗资源的。这里我们用 VueJS 和 Fastapi 的小例子演示前端传递数据，后台用 10 秒处理数据并实时反应进度给前台的实现。&lt;/p&gt;</summary>
    
    
    
    <category term="网站搭建" scheme="https://blog.xiaohanys.top/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    
    
    <category term="Python" scheme="https://blog.xiaohanys.top/tags/Python/"/>
    
    <category term="前端" scheme="https://blog.xiaohanys.top/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="后端" scheme="https://blog.xiaohanys.top/tags/%E5%90%8E%E7%AB%AF/"/>
    
  </entry>
  
</feed>
