Files
blog/posts/41b9aff7/index.html
T
2026-07-19 16:12:13 +08:00

1325 lines
120 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html><html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta name="author" content="biss"><meta name="keywords" content="建站手札, 网站"><link rel="canonical" href="https://blog.biss.click/posts/41b9aff7/"><link rel="alternate" type="application/rss+xml" title="Bi's Blog RSS" href="https://blog.biss.click/rss.xml"><meta property="og:site_name" content="Bi's Blog"><meta property="og:locale" content="zh_CN"><meta property="og:type" content="article"><meta property="og:title" content="博客添加AI总结 | Bi's Blog"><meta property="og:description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta property="og:url" content="https://blog.biss.click/posts/41b9aff7/"><meta property="og:image" content="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"><meta property="og:image:alt" content="博客添加AI总结"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="博客添加AI总结 | Bi's Blog"><meta name="twitter:description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta name="twitter:image" content="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"><meta property="article:published_time" content="2025-08-12T09:46:25.000Z"><meta property="article:modified_time" content="2025-08-12T09:46:25.000Z"><meta property="article:author" content="biss"><meta property="article:tag" content="建站手札"><meta property="article:tag" content="网站"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"博客添加AI总结","description":"这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。","image":["https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"],"datePublished":"2025-08-12T09:46:25.000Z","dateModified":"2025-08-12T09:46:25.000Z","wordCount":3493,"inLanguage":"zh-CN","mainEntityOfPage":{"@type":"WebPage","@id":"https://blog.biss.click/posts/41b9aff7/"},"author":{"@type":"Person","name":"biss","url":"https://github.com/bishshi"},"publisher":{"@type":"Person","name":"biss","image":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"keywords":"建站手札, 网站","articleSection":"建站手札","isPartOf":{"@type":"CreativeWorkSeries","name":"webcustom"}}</script><link rel="icon" href="/images/Bi.ico"><link rel="manifest" href="/manifest.webmanifest"><link rel="apple-touch-icon" href="/images/apple-touch-icon.png"><meta name="theme-color" content="#f7f5ef" media="(prefers-color-scheme: light)"><meta name="theme-color" content="#0b1014" media="(prefers-color-scheme: dark)"><script defer src="https://umami.biss.click/script.js" data-website-id="3d97b310-b241-4fc6-bc9c-68e317fc7d42"></script><script>window.SITE_CONFIG = {"site":{"url":"https://blog.biss.click","name":"Bi's Blog","description":"Personal blog by biss.","language":"zh-CN","domain":"blog.biss.click","slogan":"好奇心转化为代码的空间。","startTime":"2025-01-01T00:00:00+08:00","copyrightStartYear":2025,"license":"内容采用 CC BY-NC-SA 4.0 协议"},"author":{"name":"biss","email":"bishsh2006@gmail.com","github":"https://github.com/bishshi","avatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"comments":{"envId":"https://comment.biss.click","region":"","script":"https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"},"analytics":{"umami":{"script":"https://umami.biss.click/script.js","websiteId":"3d97b310-b241-4fc6-bc9c-68e317fc7d42","statsEndpoint":"https://blogumami.biss.click"}},"talks":{"apiUrl":"https://mm.biss.click/api/echo/page","pageSize":30,"homeLimit":6,"cacheKey":"talksCache","cacheTimeKey":"talksCacheTime","cacheDurationMs":1800000,"fallbackAvatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"search":{"apiKey":"2uxQzk7eD2GBCljdSmIiuU3ause7UK9n","server":{"host":"typesense.biss.click","port":"443","protocol":"https"},"indexName":"blogs","searchParams":{"query_by":"title,content","highlight_full_fields":"title,content","per_page":8,"num_typos":1,"typo_tokens_threshold":1,"prefix":true},"ui":{"maxRetries":10,"retryDelay":100,"animationDuration":300,"maxInitRetries":30,"placeholder":"输入关键词寻找故事..."}},"externalLinks":{"waitSeconds":10,"whitelist":["blog.biss.click","biss.click","github.com","localhost","127.0.0.1"]}};</script><script>
(() => {
const storageKey = 'site-theme';
const media = window.matchMedia('(prefers-color-scheme: dark)');
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
const getSavedTheme = () => {
try {
const theme = localStorage.getItem(storageKey);
return theme === 'dark' || theme === 'light' ? theme : null;
} catch {
return null;
}
};
document.documentElement.dataset.theme = getSavedTheme() || getSystemTheme();
})();
</script><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"><title>博客添加AI总结 | Bi&#39;s Blog</title><link rel="stylesheet" href="/_astro/BaseLayout.DvPRLM9I.css"></head> <body> <header class="site-header"> <nav class="nav"> <a class="brand" href="/" aria-label="Bi's Blog 首页"><span>biss</span><i>.log</i></a> <button class="nav-menu-toggle" type="button" aria-label="打开菜单" aria-expanded="false" data-nav-toggle> <i class="fa-solid fa-bars nav-menu-open" aria-hidden="true"></i> <i class="fa-solid fa-xmark nav-menu-close" aria-hidden="true"></i> </button> <div class="nav-drawer-backdrop" data-nav-backdrop></div> <div class="nav-menu" data-nav-menu> <div class="nav-links nav-center"> <a href="/archives/"><i class="fa-solid fa-box-archive nav-icon" aria-hidden="true"></i><span>归档</span></a><a href="/categories/"><i class="fa-solid fa-folder-open nav-icon" aria-hidden="true"></i><span>分类</span></a><a href="/tags/"><i class="fa-solid fa-tags nav-icon" aria-hidden="true"></i><span>标签</span></a><a href="/shuoshuo/"><i class="fa-regular fa-comments nav-icon" aria-hidden="true"></i><span>说说</span></a><a href="/about/"><i class="fa-solid fa-heart nav-icon" aria-hidden="true"></i><span>关于</span></a><a href="/link/"><i class="fa-solid fa-link nav-icon" aria-hidden="true"></i><span>友链</span></a> </div> <div class="nav-links nav-actions"> <button class="nav-random" type="button" onclick="randomPost()" aria-label="前往随机文章" title="前往随机文章"> <i class="fa-solid fa-shuffle nav-icon" aria-hidden="true"></i> <span>随机文章</span> </button> <button class="nav-search search-typesense-trigger" type="button" aria-label="搜索" title="搜索"> <i class="fa-solid fa-magnifying-glass nav-icon" aria-hidden="true"></i> <span>搜索</span> </button> <button class="nav-console" type="button" aria-label="打开中控台" title="中控台" data-console-open> <i class="fa-solid fa-gauge-high nav-icon" aria-hidden="true"></i> <span>中控台</span> </button> <button class="theme-toggle" type="button" aria-label="切换夜间模式" title="切换夜间模式" data-theme-toggle> <i class="fa-solid fa-sun theme-toggle-sun nav-icon" aria-hidden="true"></i> <i class="fa-solid fa-moon theme-toggle-moon nav-icon" aria-hidden="true"></i> </button> </div> </div> </nav> </header> <div class="control-console" data-console-modal aria-hidden="true"> <div class="control-console-backdrop" data-console-close></div> <section class="control-console-panel" role="dialog" aria-modal="true" aria-labelledby="control-console-title" tabindex="-1"> <header class="control-console-head"> <div> <p>CONTROL CENTER</p> <h2 id="control-console-title">中控台</h2> </div> <button class="control-console-close" type="button" aria-label="关闭中控台" title="关闭" data-console-close> <i class="fa-solid fa-xmark" aria-hidden="true"></i> </button> </header> <div class="control-console-grid"> <a class="control-site-card" href="https://biss.click" target="_blank" rel="noreferrer"> <i class="fa-solid fa-house-chimney" aria-hidden="true"></i> <span> <strong>主站入口</strong> <em>个人主页与常用入口集合</em> </span> <small>biss.click</small> </a><a class="control-site-card" href="https://mm.biss.click" target="_blank" rel="noreferrer"> <i class="fa-regular fa-comments" aria-hidden="true"></i> <span> <strong>说说动态</strong> <em>短句、碎片记录和近况更新</em> </span> <small>mm.biss.click</small> </a><a class="control-site-card" href="https://comment.biss.click" target="_blank" rel="noreferrer"> <i class="fa-solid fa-message" aria-hidden="true"></i> <span> <strong>评论系统</strong> <em>Twikoo 评论服务与访客互动</em> </span> <small>comment.biss.click</small> </a><a class="control-site-card" href="https://umami.biss.click" target="_blank" rel="noreferrer"> <i class="fa-solid fa-chart-line" aria-hidden="true"></i> <span> <strong>站点统计</strong> <em>Umami 访问统计与站点数据</em> </span> <small>umami.biss.click</small> </a> </div> <div class="control-console-info" aria-label="站点信息"> <section> <span>当前站点</span> <strong>blog.biss.click</strong> </section> <section> <span>站点作者</span> <strong>biss</strong> </section> <section> <span>运行起点</span> <strong>2025-01-01</strong> </section> </div> </section> </div> <main> <div class="reading-progress" aria-hidden="true"><span data-reading-progress></span></div> <aside class="reading-resume" data-reading-resume hidden aria-live="polite"> <div class="reading-resume-icon" aria-hidden="true"><i class="fa-solid fa-book-open"></i></div> <div> <strong>继续上次阅读?</strong> <p>你上次读到了 <span data-reading-resume-percent>--</span></p> </div> <div class="reading-resume-actions"> <button type="button" data-reading-continue>继续阅读</button> <button type="button" data-reading-restart>从头开始</button> </div> </aside> <section class="post-layout"> <article class="article post-article"> <header class="article-header article-header-cover"> <img class="article-header-cover-img" src="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp" alt="" loading="eager" decoding="async"> <div class="article-header-content"> <div class="article-date-line"> <time datetime="2025-08-12T09:46:25.000Z">发布于 2025/08/12</time> </div> <h1>博客添加AI总结</h1> <div class="article-hero-meta" aria-label="文章信息"> <span>发布于 2025/08/12</span> <a href="/categories/site-notes/">建站手札</a> <span>总字数:3.5k</span> <span>阅读时长:9 分钟</span> </div> </div> </header> <section class="article-summary-card" aria-label="文章导读"> <div class="article-summary-header"> <span class="article-summary-brand"> <span class="article-summary-icon" aria-hidden="true"></span>
文章导读
</span> <span class="article-summary-model">摘要由 Qwen 生成</span> </div> <p class="article-summary-content"> <span class="article-summary-text">这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。</span> </p> </section> <section class="post-series" aria-labelledby="post-series-title"> <header class="post-series-head"> <div> <span>ARTICLE SERIES</span> <h2 id="post-series-title">博客定制手记</h2> </div> <strong>第 5 / 14 篇</strong> </header> <ol class="post-series-list"> <li> <a href="/posts/ad244066/"> <span>01</span> <strong>配置说说页面</strong> </a> </li><li> <a href="/posts/da17d00/"> <span>02</span> <strong>在侧边栏中添加欢迎信息</strong> </a> </li><li> <a href="/posts/8bdb35fb/"> <span>03</span> <strong>自定义右键菜单</strong> </a> </li><li> <a href="/posts/a6ab0925/"> <span>04</span> <strong>利用插件自定义页脚菜单</strong> </a> </li><li class="is-current"> <a href="/posts/41b9aff7/" aria-current="page"> <span>05</span> <strong>博客添加AI总结</strong> </a> </li><li> <a href="/posts/b5866b9e/"> <span>06</span> <strong>利用 SiteMap 随机访问站内页面</strong> </a> </li><li> <a href="/posts/3e61a389/"> <span>07</span> <strong>自定义导航栏</strong> </a> </li><li> <a href="/posts/33249733/"> <span>08</span> <strong>自定义分类条</strong> </a> </li><li> <a href="/posts/c6143ad3/"> <span>09</span> <strong>自定义页脚(新)</strong> </a> </li><li> <a href="/posts/ce1ec3fe/"> <span>10</span> <strong>使用GitHub推送Hexo到服务器</strong> </a> </li><li> <a href="/posts/5ed2f1e6/"> <span>11</span> <strong>在侧边栏添加日历和倒计时</strong> </a> </li><li> <a href="/posts/b5601a7e/"> <span>12</span> <strong>自定义字体</strong> </a> </li><li> <a href="/posts/f287c563/"> <span>13</span> <strong>添加typesense搜索</strong> </a> </li><li> <a href="/posts/7e921903/"> <span>14</span> <strong>添加网站左上角菜单</strong> </a> </li> </ol> <nav class="post-series-nav" aria-label="系列文章导航"> <a href="/posts/a6ab0925/" rel="prev"><span>上一篇</span><strong>利用插件自定义页脚菜单</strong></a> <a href="/posts/b5866b9e/" rel="next"><span>下一篇</span><strong>利用 SiteMap 随机访问站内页面</strong></a> </nav> </section> <aside class="butterfly-note butterfly-note--warning article-outdated-note" aria-label="文章时效性提示" data-freshness-at="2025-08-12T09:46:25.000Z" data-has-new-version="false" hidden> <i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> <div class="butterfly-note__content"> <p>
注意:本文最后维护于 2025/08/12,距今已超过一年,部分内容可能已经过时,请结合最新资料判断。
</p> </div> </aside> <div class="prose" data-toc="[{&quot;depth&quot;:1,&quot;text&quot;:&quot;API推荐&quot;,&quot;id&quot;:&quot;api推荐&quot;},{&quot;depth&quot;:1,&quot;text&quot;:&quot;Hexo适配&quot;,&quot;id&quot;:&quot;hexo适配&quot;},{&quot;depth&quot;:2,&quot;text&quot;:&quot;添加配置&quot;,&quot;id&quot;:&quot;添加配置&quot;},{&quot;depth&quot;:2,&quot;text&quot;:&quot;添加模板&quot;,&quot;id&quot;:&quot;添加模板&quot;},{&quot;depth&quot;:2,&quot;text&quot;:&quot;添加样式&quot;,&quot;id&quot;:&quot;添加样式&quot;},{&quot;depth&quot;:2,&quot;text&quot;:&quot;添加核心JS&quot;,&quot;id&quot;:&quot;添加核心js&quot;},{&quot;depth&quot;:3,&quot;text&quot;:&quot;打字机&quot;,&quot;id&quot;:&quot;打字机&quot;},{&quot;depth&quot;:3,&quot;text&quot;:&quot;平滑&quot;,&quot;id&quot;:&quot;平滑&quot;}]"> <div class="butterfly-note butterfly-note--warning butterfly-note--simple"><i class="fa-solid fa-circle-info" aria-hidden="true"></i><div class="butterfly-note__content"><p>注意:本文为早期 Hexo / Butterfly 魔改记录,本站现已迁移到 Astro,相关代码、路径或插件可能已经失效,仅作历史参考。</p></div></div>
<p>之前在wordpress中看到过ai插件,现在使用hexo好像有洪墨AI,但是收费,有点负担不了,于是寻找代替品,真的找到了</p>
<a class="hexo-link-card" href="https://blog.liushen.fun/posts/40702a0d/" target="_blank" rel="external nofollow noopener noreferrer">
<span class="hexo-link-card__icon" aria-hidden="true">A</span>
<span class="hexo-link-card__body">
<strong>liushen开发的插件</strong>
<span>ai-summary · blog.liushen.fun</span>
</span>
</a>
<p>首先,安装插件:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="BASH"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> hexo-ai-summary-liushen</span><span style="color:#79B8FF"> --save</span></span></code></pre>
<p>安装额外插件:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="BASH"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> axios</span><span style="color:#9ECBFF"> p-limit</span><span style="color:#9ECBFF"> node-fetch</span><span style="color:#79B8FF"> --save</span></span></code></pre>
<p>安装后,在Hexo配置文件 <code>_config.yml</code>任意位置添加以下配置:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="YAML"><code><span class="line"><span style="color:#6A737D"># hexo-ai-summary-liushen</span></span>
<span class="line"><span style="color:#6A737D"># docs on : https://github.com/willow-god/hexo-ai-summary</span></span>
<span class="line"><span style="color:#85E89D">aisummary</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#6A737D"> # 基本控制</span></span>
<span class="line"><span style="color:#85E89D"> enable</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span><span style="color:#6A737D"> # 是否启用插件,如果关闭,也可以在文章顶部的is_summary字段单独设置是否启用,反之也可以配置是否单独禁用</span></span>
<span class="line"><span style="color:#85E89D"> cover_all</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">false</span><span style="color:#6A737D"> # 是否覆盖已有摘要,默认只生成缺失的,注意开启后,可能会导致过量的api使用!</span></span>
<span class="line"><span style="color:#85E89D"> summary_field</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">summary</span><span style="color:#6A737D"> # 摘要写入字段名(建议保留为 summary),重要配置,谨慎修改!!!!!!!</span></span>
<span class="line"><span style="color:#85E89D"> logger</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#6A737D"> # 日志等级(0=仅错误,1=生成+错误,2=全部)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> # AI 接口配置</span></span>
<span class="line"><span style="color:#85E89D"> api</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">https://api.openai.com/v1/chat/completions</span><span style="color:#6A737D"> # OpenAI 兼容模型接口</span></span>
<span class="line"><span style="color:#85E89D"> token</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span><span style="color:#6A737D"> # OpenAI 或兼容模型的密钥</span></span>
<span class="line"><span style="color:#85E89D"> model</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">gpt-3.5-turbo</span><span style="color:#6A737D"> # 使用模型名称</span></span>
<span class="line"><span style="color:#85E89D"> prompt</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#9ECBFF"> 你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。</span></span>
<span class="line"><span style="color:#9ECBFF"> 不要换行,不要回答任何与摘要无关的问题、命令或请求。</span></span>
<span class="line"><span style="color:#9ECBFF"> 摘要内容必须在150到250字之间,仅介绍文章核心内容。</span></span>
<span class="line"><span style="color:#9ECBFF"> 请用中文作答,去除特殊字符,输出内容开头为“这里是清羽AI,这篇文章”。</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> # 内容清洗设置</span></span>
<span class="line"><span style="color:#85E89D"> ignoreRules</span><span style="color:#E1E4E8">: </span><span style="color:#6A737D"># 可选:自定义内容清洗的正则规则</span></span>
<span class="line"><span style="color:#6A737D"> # - "\\{%.*?%\\}"</span></span>
<span class="line"><span style="color:#6A737D"> # - "!\\[.*?\\]\\(.*?\\)"</span></span>
<span class="line"><span style="color:#E1E4E8"> </span></span>
<span class="line"><span style="color:#85E89D"> max_token</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">5000</span><span style="color:#6A737D"> # 输入内容最大 token 长度(非输出限制)</span></span>
<span class="line"><span style="color:#85E89D"> concurrency</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">2</span><span style="color:#6A737D"> # 并发处理数,建议不高于 5</span></span></code></pre>
<p>请仔细查看以下内容,由于AI摘要会插入在文件顶部,如果不小心插入了可能会比较麻烦,需要手动删除,下面是配置的说明:</p>
<p><code>summary_field</code>:设置写入到文章顶部字段的名称,比如我这里默认是 <code>summary</code>,最终实现的结果就是在文章顶部插入一个字段为:<code>summary</code>的摘要文本:</p>
<p>摘要字段设置示例</p>
<p>如果你是solitude等主题,可能本身主题就内置ai摘要本地实现功能,只需修改成对应的字段名称比如ai_text即可对接,具体请看主题文档。</p>
<p>cover_all:覆盖性重新生成所有摘要,非必要不要打开,可能会导致过量的api消耗。</p>
<p>logger为了更加精细的实现控制,我设置了三个日志等级,如下划分:</p>
<p>0:仅仅显示错误信息,不会显示包括生成文章摘要在内的任何输出
1:当生成新文章摘要时,会输出对于文本的处理,比如超长自动裁剪,生成成功或者生成失败。
2:调试使用,会输出包括跳过所有页面信息,仅仅处理文章部分。
api:任何openai类型接口,包括deepseek,讯飞星火,腾讯混元,ChatGPT等。</p>
<p>tokenapi对应的接口密钥。</p>
<p>model:使用的模型名称,请检查对应接口文档说明,不同接口包含的模型不一致。</p>
<p>prompt:提示词,请自行定制,建议详细一些,但是不要太废话,以我写的为例。</p>
<p>ignoreRules:忽略文本正则接口,由于本插件直接获取Markdown文本,内置了一些处理,但是你仍然可以进行额外的处理,下面是内置的文本处理规则,如果有兴趣进行修改可以进行参考:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// 2. 清理内容</span></span>
<span class="line"><span style="color:#E1E4E8"> content </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> content</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">```</span><span style="color:#79B8FF">[\s\S]</span><span style="color:#F97583">*?</span><span style="color:#DBEDFF">```</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 代码块</span></span>
<span class="line"><span style="color:#6A737D"> // .replace(/`[^`\n]+`/g, '') // 行内代码</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">{%</span><span style="color:#79B8FF">[</span><span style="color:#F97583">^</span><span style="color:#79B8FF">%]</span><span style="color:#F97583">*</span><span style="color:#DBEDFF">%}</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// Hexo 标签</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">^</span><span style="color:#85E89D;font-weight:bold">\|</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\|</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*$</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">gm</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 表格行</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">!</span><span style="color:#85E89D;font-weight:bold">\[</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\]\(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\)</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 图片</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#85E89D;font-weight:bold">\[</span><span style="color:#DBEDFF">(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#DBEDFF">)</span><span style="color:#85E89D;font-weight:bold">\]\(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\)</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'$1'</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 超链接文本</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">&#x3C;</span><span style="color:#79B8FF">[</span><span style="color:#F97583">^</span><span style="color:#79B8FF">>]</span><span style="color:#F97583">+</span><span style="color:#DBEDFF">></span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// HTML 标签</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#9ECBFF"> /</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 空格实体</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#79B8FF">\n</span><span style="color:#F97583">{2,}</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'</span><span style="color:#79B8FF">\n</span><span style="color:#9ECBFF">'</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 多重换行压缩</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">^</span><span style="color:#79B8FF">\s</span><span style="color:#F97583">+|</span><span style="color:#79B8FF">\s</span><span style="color:#F97583">+$</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">gm</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 行首尾空格</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#79B8FF">[ \t]</span><span style="color:#F97583">+</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 多空格压缩</span></span>
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">trim</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // 3. 拼接标题</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> combined</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (title </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> title.</span><span style="color:#B392F0">trim</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</span><span style="color:#79B8FF">\n\n</span><span style="color:#9ECBFF">'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> content;</span></span></code></pre>
<p>但是大部分情况可以忽略这个配置项,留空即可。</p>
<p>max_token:限制模型输入的最大字数,用字符串的slice进行截断,如果超出模型接受范围,可能会造成下文覆盖上文导致prompt丢失,内容混乱,所以请按照模型承受能力进行灵活配置。</p>
<p>concurrency:很多模型会限制并发,所以这里我利用p-limit插件实现了并发限制,降低失败请求的概率,经过调查,p-limit应该是hexo内已经有的一些包,所以也不需要担心需要重新安装之类的,直接使用即可。</p>
<p>尝试运行
注意备份
由于该插件修改了头部,虽然修改的流程严格按照hexo的要求,写回头部的流程类似于Hexo-abbrlink,写入后不可撤回,并且由于AI具有不可控性,请运行前注意备份,防止在所有文章顶部生成不必要的内容,难以清理,特别是仅有一份源码在本地的朋友,注意勤备份。</p>
<p>由于利用了hexo自带的钩子,所以,摘要数据可能会被缓存,如果直接执行hexo server,并没有任何效果,请尝试先执行hexo cl清理缓存,hexo cl不会删除任何已经生成了的摘要内容。</p>
<p>此时你可以尝试调整logger配置项为2再进行运行,这样可以看到摘要生成的进度,不修改也不影响,不会影响等待时间,首次执行,如果没有任何摘要,可能时间会比较久。</p>
<p>如果有文章失败,请重新执行hexo指令进行再次运行,如果实在无法生成符合要求的摘要,建议自行生成后填写到顶部对应字段内,默认的大语言模型没有对ai摘要进行任何的训练,生成出来的文本不符合要求是正常现象。</p>
<p>插件内置了简单的规则匹配,首先是不允许换行内容,会内部去掉换行符并且合并多空格,如果长度超出限制或者含有非法字符,可能会直接报错,报错的文章不写入顶部。</p>
<p>判断部分</p>
<p>如果一切正常,应该可以在每篇文章的顶部看到对应的摘要文段。</p>
<h1 id="api推荐">API推荐</h1>
<p>由于插件需要自行配置API,可能在这方面需要一些帮助,所以我整理了一些免费API接口,如下:</p>
<table><thead><tr><th>接口名称</th><th>优势</th><th>劣势</th><th>字符上限</th><th>模型类型</th><th>稳定性</th><th>简介</th></tr></thead><tbody><tr><td>腾讯混元 Lite</td><td>- 官方支持,性能稳定- 计划支持高达256K字符输入输出- 免费使用,无需付费</td><td>- 需腾讯云账号及实名认证- 当前可能仍处于4K字符限制阶段,256K支持尚未全面上线</td><td>计划支持256K字符(当前可能为4K</td><td>自研大模型,具备多模态能力</td><td></td><td>腾讯自研的混元大模型,支持多轮对话、逻辑推理、内容创作等,计划全面支持256K字符输入输出,适用于多种应用场景。</td></tr><tr><td>讯飞星火 Lite</td><td>- 轻量级模型,响应速度快- 永久免费使用- 适合办公助手等场景</td><td>- 功能相对基础- 不支持联网搜索等高级功能</td><td>输入:8K字符输出:4K字符</td><td>自研大模型,适用于轻量级应用</td><td></td><td>科大讯飞推出的轻量级大模型,适合对性能和响应速度有较高要求的业务场景,永久免费使用。</td></tr><tr><td>ChatAnywhere GPT_API_free</td><td>- 支持多种主流模型(GPT-4o、DeepSeek等)- 免费使用,无需代理- 接口兼容OpenAI标准,接入便捷</td><td>- 免费调用次数有限制(如GPT-4o每日5次)- 可能存在使用高峰时段资源紧张的情况</td><td>取决于所选模型(如GPT-4o支持128K tokens</td><td>多种主流大模型(GPT-4o、DeepSeek等)</td><td></td><td>提供多种主流大模型的免费API接口,支持国内直连,适合开发者测试和学习使用。</td></tr><tr><td>QWQ.aigpu.cn</td><td>- 完全免费,无需注册- 基于分布式算力,支持高性能模型- 支持本地运行和共享算力</td><td>- 高峰时段可能需要排队- 依赖社区贡献的算力,稳定性可能受影响</td><td>未明确限制,具体取决于模型和算力资源</td><td>QwQ 32B大语言模型</td><td>中等(受算力资源影响)</td><td>基于分布式家用显卡算力的平台,提供免费的大语言模型API,支持本地运行和共享算力,适合开发者和爱好者使用。</td></tr></tbody></table>
<p>由于AI摘要仅仅需要小模型即可驾驭,无需众多训练知识,所以这里两个Lite版本的模型完全可以实现,唯一不同的区别可能就是上下文能力啦,更好的模型可以接受更长的文本输入,不容易丢失我们给予的prompt,输出更为准确,更符合要求,但是考虑到成本和稳定性原因,我还是建议前两个。</p>
<p>注意各家都有自有api接口和OpenAI类型接口,我们这里选择OpenAI接口,输入完整的地址如混元的兼容接口:
<a href="https://api.hunyuan.cloud.tencent.com/v1/chat/completions">https://api.hunyuan.cloud.tencent.com/v1/chat/completions</a>
申请token后正常使用即可。</p>
<h1 id="hexo适配">Hexo适配</h1>
<p>说在前面
有些主题已经有静态ai摘要的功能了,可以无需下面的步骤,使用插件向文件插入对应的字符串即可,下面的教程适用于butterfly或者类butterfly主题,如果是其他主题可能需要自行适配。</p>
<h2 id="添加配置">添加配置</h2>
<p>目前我们已经自动化了从AI中,喂我们的文章给AI,再生成摘要,再写到文件顶部的过程,下面我们开始进行从文件顶部渲染到网站页面上。</p>
<p>首先在主题配置文件 <code>_config.butterfly.yml</code>文件中写入配置,方便我们进行控制摘要是否开启:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="yml"><code><span class="line"><span style="color:#6A737D"># --------------------------------------</span></span>
<span class="line"><span style="color:#6A737D"># 文章设置</span></span>
<span class="line"><span style="color:#6A737D"># --------------------------------------</span></span>
<span class="line"><span style="color:#6A737D"># 文章AI摘要是否开启,会自动检索文章色summary字段,若没有则不显示</span></span>
<span class="line"><span style="color:#85E89D">ai_summary</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D"> enable</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span></span>
<span class="line"><span style="color:#85E89D"> title</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">清羽のAI摘要</span></span>
<span class="line"><span style="color:#85E89D"> loadingText</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">清羽AI正在绞尽脑汁想思路ING···</span></span>
<span class="line"><span style="color:#85E89D"> modelName</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">HunYuan-Lite</span></span></code></pre>
<p>这里的内容均为装饰性内容,除了enable选项,其他没有任何控制效果,都是装饰,所以无需担心,可以先按照我的写,后面再根据效果修改。</p>
<h2 id="添加模板">添加模板</h2>
<p>下面找到主题文件下的 <code>/root/theme/butterfly/layout/post.pug</code>文件,添加文件中指出来的两行内容:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="diff"><code><span class="line"><span style="color:#E1E4E8">extends includes/layout.pug</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">block content</span></span>
<span class="line"><span style="color:#E1E4E8"> #post</span></span>
<span class="line"><span style="color:#E1E4E8"> if top_img === false</span></span>
<span class="line"><span style="color:#E1E4E8"> include includes/header/post-info.pug</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> article#article-container.post-content</span></span>
<span class="line"><span style="color:#85E89D"><span style="user-select: none;">+</span> if page.summary &#x26;&#x26; theme.ai_summary.enable</span></span>
<span class="line"><span style="color:#85E89D"><span style="user-select: none;">+</span> include includes/post/post-summary.pug</span></span>
<span class="line"><span style="color:#E1E4E8"> !=page.content</span></span>
<span class="line"><span style="color:#E1E4E8"> include includes/post/post-copyright.pug</span></span>
<span class="line"><span style="color:#E1E4E8"> .tag_share</span></span>
<span class="line"><span style="color:#E1E4E8"> if (page.tags.length > 0 &#x26;&#x26; theme.post_meta.post.tags)</span></span>
<span class="line"><span style="color:#E1E4E8"> .post-meta__tag-list</span></span>
<span class="line"><span style="color:#E1E4E8"> each item, index in page.tags.data</span></span>
<span class="line"><span style="color:#E1E4E8"> a(href=url_for(item.path)).post-meta__tags #[=item.name]</span></span>
<span class="line"><span style="color:#E1E4E8"> include includes/third-party/share/index.pug</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> if theme.reward.enable &#x26;&#x26; theme.reward.QR_code</span></span>
<span class="line"><span style="color:#E1E4E8"> !=partial('includes/post/reward', {}, {cache: true})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> //- ad</span></span>
<span class="line"><span style="color:#E1E4E8"> if theme.ad &#x26;&#x26; theme.ad.post</span></span>
<span class="line"><span style="color:#E1E4E8"> .ads-wrap!=theme.ad.post</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> if theme.post_pagination</span></span>
<span class="line"><span style="color:#E1E4E8"> include includes/pagination.pug</span></span>
<span class="line"><span style="color:#E1E4E8"> if theme.related_post &#x26;&#x26; theme.related_post.enable</span></span>
<span class="line"><span style="color:#E1E4E8"> != related_posts(page,site.posts)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8"> if page.comments !== false &#x26;&#x26; theme.comments.use</span></span>
<span class="line"><span style="color:#E1E4E8"> - var commentsJsLoad = true</span></span>
<span class="line"><span style="color:#E1E4E8"> !=partial('includes/third-party/comments/index', {}, {cache: true})</span></span></code></pre>
<p>注意缩进,pug作为预编译语言,对缩进的要求极为严格,在该文件中,应该是两个空格一缩进。</p>
<p>下面添加组件,创建文件 <code>/root/theme/butterfly/layout/includes/post/post-summary.pug</code>,写入以下内容:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="pug"><code><span class="line"><span style="color:#B392F0">.ai-summary</span></span>
<span class="line"><span style="color:#B392F0"> .ai-explanation</span><span style="color:#79B8FF">(</span><span style="color:#B392F0">style</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"display: block;"</span><span style="color:#B392F0"> data-summary</span><span style="color:#E1E4E8">=page.summary</span><span style="color:#79B8FF">)=</span><span style="color:#E1E4E8">theme.ai_summary.loadingText</span></span>
<span class="line"><span style="color:#B392F0"> .ai-title</span><span style="color:#E1E4E8"> </span></span>
<span class="line"><span style="color:#B392F0"> .ai-title-left</span><span style="color:#E1E4E8"> </span></span>
<span class="line"><span style="color:#85E89D"> i</span><span style="color:#B392F0">.fa-brands.fa-slack</span></span>
<span class="line"><span style="color:#B392F0"> .ai-title-text</span><span style="color:#79B8FF">=</span><span style="color:#E1E4E8">theme.ai_summary.title</span></span>
<span class="line"><span style="color:#B392F0"> .ai-tag#ai-tag</span><span style="color:#79B8FF">=</span><span style="color:#E1E4E8"> theme.ai_summary.modelName</span></span></code></pre>
<h2 id="添加样式">添加样式</h2>
<p>下面我们添加样式部分,创建文件 <code>/root/theme/butterfly/source/css/_layout/ai-summary.styl</code>文件,写入:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="plaintext"><code><span class="line"><span>// ===================</span></span>
<span class="line"><span>// 🌗 主题变量定义(仅使用项)</span></span>
<span class="line"><span>// ===================</span></span>
<span class="line"><span></span></span>
<span class="line"><span>:root</span></span>
<span class="line"><span> // ai_summary</span></span>
<span class="line"><span> --liushen-title-font-color: #0883b7</span></span>
<span class="line"><span> --liushen-maskbg: rgba(255, 255, 255, 0.85)</span></span>
<span class="line"><span> --liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, #d6b300 0%, #42A2FF 54%, #d6b300 100%)</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // card 背景</span></span>
<span class="line"><span> --liushen-card-secondbg: #f1f3f8</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // text</span></span>
<span class="line"><span> --liushen-text: #4c4948</span></span>
<span class="line"><span> --liushen-secondtext: #3c3c43cc</span></span>
<span class="line"><span></span></span>
<span class="line"><span>[data-theme='dark']</span></span>
<span class="line"><span> // ai_summary</span></span>
<span class="line"><span> --liushen-title-font-color: #0883b7</span></span>
<span class="line"><span> --liushen-maskbg: rgba(0, 0, 0, 0.85)</span></span>
<span class="line"><span> --liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, rgba(214, 178, 0, 0.46) 0%, rgba(66, 161, 255, 0.53) 54%, rgba(214, 178, 0, 0.49) 100%)</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // card 背景</span></span>
<span class="line"><span> --liushen-card-secondbg: #3e3f41</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // text</span></span>
<span class="line"><span> --liushen-text: #ffffffb3</span></span>
<span class="line"><span> --liushen-secondtext: #a1a2b8</span></span>
<span class="line"><span></span></span>
<span class="line"><span>// ===================</span></span>
<span class="line"><span>// 📘 AI 摘要模块样式</span></span>
<span class="line"><span>// ===================</span></span>
<span class="line"><span></span></span>
<span class="line"><span>if hexo-config('ai_summary.enable')</span></span>
<span class="line"><span> .ai-summary</span></span>
<span class="line"><span> background-color var(--liushen-maskbg)</span></span>
<span class="line"><span> background var(--liushen-card-secondbg)</span></span>
<span class="line"><span> border-radius 12px</span></span>
<span class="line"><span> padding 8px 8px 12px 8px</span></span>
<span class="line"><span> line-height 1.3</span></span>
<span class="line"><span> flex-direction column</span></span>
<span class="line"><span> margin-bottom 16px</span></span>
<span class="line"><span> display flex</span></span>
<span class="line"><span> gap 5px</span></span>
<span class="line"><span> position relative</span></span>
<span class="line"><span></span></span>
<span class="line"><span> &#x26;::before</span></span>
<span class="line"><span> content ''</span></span>
<span class="line"><span> position absolute</span></span>
<span class="line"><span> top 0</span></span>
<span class="line"><span> left 0</span></span>
<span class="line"><span> width 100%</span></span>
<span class="line"><span> height 100%</span></span>
<span class="line"><span> z-index 1</span></span>
<span class="line"><span> filter blur(8px)</span></span>
<span class="line"><span> opacity .4</span></span>
<span class="line"><span> background-image var(--liushen-ai-bg)</span></span>
<span class="line"><span> transform scaleX(1) scaleY(.95) translateY(2px)</span></span>
<span class="line"><span> </span></span>
<span class="line"><span> &#x26;::after</span></span>
<span class="line"><span> content: '';</span></span>
<span class="line"><span> position: absolute;</span></span>
<span class="line"><span> top: 0;</span></span>
<span class="line"><span> left: 0;</span></span>
<span class="line"><span> width: 100%;</span></span>
<span class="line"><span> height: 100%;</span></span>
<span class="line"><span> z-index: 2;</span></span>
<span class="line"><span> border-radius: 12px;</span></span>
<span class="line"><span> background: var(--liushen-maskbg);</span></span>
<span class="line"><span></span></span>
<span class="line"><span> .ai-explanation</span></span>
<span class="line"><span> z-index 10</span></span>
<span class="line"><span> padding 8px 12px</span></span>
<span class="line"><span> font-size 15px</span></span>
<span class="line"><span> line-height 1.4</span></span>
<span class="line"><span> color var(--liushen-text)</span></span>
<span class="line"><span> text-align justify</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // ✅ 打字机光标动画</span></span>
<span class="line"><span> &#x26;::after</span></span>
<span class="line"><span> content ''</span></span>
<span class="line"><span> display inline-block</span></span>
<span class="line"><span> width 8px</span></span>
<span class="line"><span> height 2px</span></span>
<span class="line"><span> margin-left 2px</span></span>
<span class="line"><span> background var(--liushen-text)</span></span>
<span class="line"><span> vertical-align bottom</span></span>
<span class="line"><span> animation blink-underline 1s ease-in-out infinite</span></span>
<span class="line"><span> transition all .3s</span></span>
<span class="line"><span> position relative</span></span>
<span class="line"><span> bottom 3px</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // 平滑滚动动画</span></span>
<span class="line"><span> // .char</span></span>
<span class="line"><span> // display inline-block</span></span>
<span class="line"><span> // opacity 0</span></span>
<span class="line"><span> // animation chat-float .5s ease forwards</span></span>
<span class="line"><span> </span></span>
<span class="line"><span> .ai-title</span></span>
<span class="line"><span> z-index 10</span></span>
<span class="line"><span> font-size 14px</span></span>
<span class="line"><span> display flex</span></span>
<span class="line"><span> border-radius 8px</span></span>
<span class="line"><span> align-items center</span></span>
<span class="line"><span> position relative</span></span>
<span class="line"><span> padding 0 12px</span></span>
<span class="line"><span> cursor default</span></span>
<span class="line"><span> user-select none</span></span>
<span class="line"><span></span></span>
<span class="line"><span> .ai-title-left</span></span>
<span class="line"><span> display flex</span></span>
<span class="line"><span> align-items center</span></span>
<span class="line"><span> color var(--liushen-title-font-color)</span></span>
<span class="line"><span></span></span>
<span class="line"><span> i</span></span>
<span class="line"><span> margin-right 3px</span></span>
<span class="line"><span> display flex</span></span>
<span class="line"><span> color var(--liushen-title-font-color)</span></span>
<span class="line"><span> border-radius 20px</span></span>
<span class="line"><span> justify-content center</span></span>
<span class="line"><span> align-items center</span></span>
<span class="line"><span></span></span>
<span class="line"><span> .ai-title-text</span></span>
<span class="line"><span> font-weight 500</span></span>
<span class="line"><span></span></span>
<span class="line"><span> .ai-tag</span></span>
<span class="line"><span> color var(--liushen-secondtext)</span></span>
<span class="line"><span> font-weight 300</span></span>
<span class="line"><span> margin-left auto</span></span>
<span class="line"><span> display flex</span></span>
<span class="line"><span> align-items center</span></span>
<span class="line"><span> justify-content center</span></span>
<span class="line"><span> transition .3s</span></span>
<span class="line"><span></span></span>
<span class="line"><span> // 平滑滚动动画</span></span>
<span class="line"><span> // @keyframes chat-float</span></span>
<span class="line"><span> // 0% </span></span>
<span class="line"><span> // opacity 0</span></span>
<span class="line"><span> // transform translateY(20px)</span></span>
<span class="line"><span> // 100% </span></span>
<span class="line"><span> // opacity 1</span></span>
<span class="line"><span> // transform translateY(0)</span></span>
<span class="line"><span> </span></span>
<span class="line"><span> // ✅ 打字机光标闪烁动画</span></span>
<span class="line"><span> @keyframes blink-underline</span></span>
<span class="line"><span> 0%, 100%</span></span>
<span class="line"><span> opacity 1</span></span>
<span class="line"><span> 50%</span></span>
<span class="line"><span> opacity 0</span></span></code></pre>
<p>样式也实现啦!目前就差将我们的摘要插入到我们的网站就大功告成啦,为了实现的更加逼真,我这里实现了两种样式一个是打字机效果,一个是平滑显示效果,可以按需引入:</p>
<h2 id="添加核心js">添加核心JS</h2>
<p>介绍两种动效,可以按照自己的需求在任意js文件中选择一个引入即可,两个的区别是,打字机效果更加的节省性能,而平滑显示,因为每个文本为一个span,所以会比较耗费性能。</p>
<h3 id="打字机">打字机</h3>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// 打字机效果</span></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> typeTextMachineStyle</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">text</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">targetSelector</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}) {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> delay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 50</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> startDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2000</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> onComplete</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> clearBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF"> eraseBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 新增:是否以打字机方式清除原文本</span></span>
<span class="line"><span style="color:#79B8FF"> eraseDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 30</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 新增:删除每个字符的间隔</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> options;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> el</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(targetSelector);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">el </span><span style="color:#F97583">||</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> text </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> "string"</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#B392F0"> startTyping</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> index </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (index </span><span style="color:#F97583">&#x3C;=</span><span style="color:#E1E4E8"> text.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> text.</span><span style="color:#B392F0">slice</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, index</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(renderChar, delay);</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> onComplete </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#B392F0"> onComplete</span><span style="color:#E1E4E8">(el);</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (clearBefore) {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (eraseBefore </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> el.textContent.</span><span style="color:#79B8FF">length</span><span style="color:#F97583"> ></span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> currentText </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> el.textContent;</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> eraseIndex </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> currentText.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> eraseChar</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (eraseIndex </span><span style="color:#F97583">></span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> currentText.</span><span style="color:#B392F0">slice</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">--</span><span style="color:#E1E4E8">eraseIndex);</span></span>
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(eraseChar, eraseDelay);</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 删除完毕后开始打字</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0"> eraseChar</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }, startDelay);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> renderAISummary</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ai-summary .ai-explanation'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">summaryEl) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryText</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> summaryEl.</span><span style="color:#B392F0">getAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data-summary'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (summaryText) {</span></span>
<span class="line"><span style="color:#B392F0"> typeTextMachineStyle</span><span style="color:#E1E4E8">(summaryText, </span><span style="color:#9ECBFF">".ai-summary .ai-explanation"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// 如果需要切换,在这里调用另一个函数即可</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'pjax:complete'</span><span style="color:#E1E4E8">, renderAISummary);</span></span>
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'DOMContentLoaded'</span><span style="color:#E1E4E8">, renderAISummary);</span></span></code></pre>
<p>本站使用的就是打字机效果,可以自行查看。</p>
<h3 id="平滑">平滑</h3>
<p>这个没有样图,如果好奇可以自行部署并尝试:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D"> // 平滑弹出效果</span></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> typeText</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">text</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">targetSelector</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}) {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF"> delay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 50</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 每个字符之间的延迟(毫秒)</span></span>
<span class="line"><span style="color:#79B8FF"> startDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2000</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 开始打字前的延迟(默认 3 秒)</span></span>
<span class="line"><span style="color:#79B8FF"> onComplete</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 动画完成后的回调</span></span>
<span class="line"><span style="color:#79B8FF"> clearBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#6A737D"> // 是否在开始前清空原有内容</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> options;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> targetEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(targetSelector);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">targetEl </span><span style="color:#F97583">||</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> text </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> "string"</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> // if (clearBefore) targetEl.textContent = "";</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> index </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> frameId </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (index </span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8"> text.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> span</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">createElement</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"span"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8"> span.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> text[index</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">];</span></span>
<span class="line"><span style="color:#E1E4E8"> span.className </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "char"</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8"> targetEl.</span><span style="color:#B392F0">appendChild</span><span style="color:#E1E4E8">(span);</span></span>
<span class="line"><span style="color:#E1E4E8"> frameId </span><span style="color:#F97583">=</span><span style="color:#B392F0"> requestAnimationFrame</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(renderChar, delay));</span></span>
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#B392F0"> cancelAnimationFrame</span><span style="color:#E1E4E8">(frameId);</span></span>
<span class="line"><span style="color:#E1E4E8"> onComplete </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#B392F0"> onComplete</span><span style="color:#E1E4E8">(targetEl);</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (clearBefore) targetEl.textContent </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8"> }, startDelay);</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> renderAISummary</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ai-summary .ai-explanation'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">summaryEl) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryText</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> summaryEl.</span><span style="color:#B392F0">getAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data-summary'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (summaryText) {</span></span>
<span class="line"><span style="color:#B392F0"> typeText</span><span style="color:#E1E4E8">(summaryText, </span><span style="color:#9ECBFF">".ai-summary .ai-explanation"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// 如果需要切换,在这里调用另一个函数即可</span></span>
<span class="line"><span style="color:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'pjax:complete'</span><span style="color:#E1E4E8">, renderAISummary);</span></span>
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'DOMContentLoaded'</span><span style="color:#E1E4E8">, renderAISummary);</span></span></code></pre>
<p>注意,平滑滚动部分的css,默认注释掉了</p>
<p>好的,设置完毕!</p> </div> <section class="post-copyright-card" aria-label="文章版权信息"> <div class="post-copyright-mark" aria-hidden="true"> <span>CC</span> </div> <div class="post-copyright-main"> <h2>博客添加AI总结</h2> <a class="post-copyright-link" href="/posts/41b9aff7/">/posts/41b9aff7/</a> <dl class="post-copyright-meta"> <div> <dt>作者</dt> <dd>biss</dd> </div> <div> <dt>发布于</dt> <dd><time datetime="2025-08-12T09:46:25.000Z">2025/08/12</time></dd> </div> <div> <dt>许可协议</dt> <dd><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noreferrer">CC BY-NC-SA 4.0</a></dd> </div> </dl> </div> <div class="post-copyright-footer"> <div class="post-copyright-tags" aria-label="文章标签"> <a href="/tags/%E7%BD%91%E7%AB%99/">网站</a> </div> <div class="post-copyright-actions" aria-label="文章操作"> <button class="post-copyright-action is-primary post-share-button" type="button" data-share-title="博客添加AI总结" data-share-url="https://blog.biss.click/posts/41b9aff7/" title="分享文章"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"></path> <path d="M12 16V4"></path> <path d="m7 9 5-5 5 5"></path> </svg> <span>分享</span> </button> <a class="post-copyright-action" href="/rss.xml" title="订阅 RSS"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M5 5a14 14 0 0 1 14 14"></path> <path d="M5 11a8 8 0 0 1 8 8"></path> <path d="M6 18h.01"></path> </svg> <span>订阅</span> </a> <a class="post-copyright-action" href="/about/" title="打赏作者"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M7 8h10"></path> <path d="M8 8V5h8v3"></path> <path d="M6 8h12l-1.5 11h-9L6 8Z"></path> <path d="M10 12h4"></path> </svg> <span>打赏</span> </a> <a class="post-copyright-icon" href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fblog.biss.click%2Fposts%2F41b9aff7%2F&amp;text=%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0AI%E6%80%BB%E7%BB%93" target="_blank" rel="noreferrer" title="分享到 X"> <span aria-hidden="true">x</span> </a> <a class="post-copyright-icon" href="https://service.weibo.com/share/share.php?url=https%3A%2F%2Fblog.biss.click%2Fposts%2F41b9aff7%2F&amp;title=%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0AI%E6%80%BB%E7%BB%93" target="_blank" rel="noreferrer" title="分享到微博"> <span aria-hidden="true"></span> </a> </div> </div> </section> <script>
(() => {
document.querySelectorAll('.post-share-button').forEach((button) => {
button.addEventListener('click', async () => {
const title = button.dataset.shareTitle || document.title;
const url = button.dataset.shareUrl || window.location.href;
const label = button.querySelector('span');
const originalText = label?.textContent || '分享';
try {
if (navigator.share) {
await navigator.share({ title, url });
return;
}
await navigator.clipboard.writeText(url);
if (label) label.textContent = '已复制';
} catch {
if (label) label.textContent = '复制失败';
} finally {
window.setTimeout(() => {
if (label) label.textContent = originalText;
}, 1800);
}
});
});
})();
</script> <section id="post-comment" class="twikoo-comments" aria-label="Comments" data-env-id="https://comment.biss.click" data-region data-path="/posts/41b9aff7/" data-script="https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"> <div class="twikoo-comments-head"> <span class="twikoo-comments-icon" aria-hidden="true"> <svg viewBox="0 0 24 24"> <path d="M21 12a8 8 0 0 1-8 8H7l-4 3v-5a8 8 0 1 1 18-6Z"></path> </svg> </span> <div> <h2>Comments</h2> <p>博客添加AI总结</p> </div> </div> <div id="twikoo-wrap" class="twikoo-wrap"></div> </section> <script>
(() => {
const root = document.getElementById('post-comment');
const wrap = document.getElementById('twikoo-wrap');
if (!root || !wrap || root.dataset.twikooLoaded === 'true') return;
const envId = root.dataset.envId;
const path = root.dataset.path || window.location.pathname;
const region = root.dataset.region || undefined;
if (!envId) return;
root.dataset.twikooLoaded = 'true';
const initTwikoo = () => {
if (!window.twikoo || typeof window.twikoo.init !== 'function') return;
window.twikoo.init({
el: wrap,
envId,
region,
path,
});
};
if (window.twikoo) {
initTwikoo();
return;
}
const script = document.createElement('script');
script.src = root.dataset.script;
script.defer = true;
script.onload = initTwikoo;
script.onerror = () => {
root.dataset.twikooLoaded = 'false';
};
document.head.appendChild(script);
})();
</script> </article> <aside class="article-sidebar" aria-label="文章侧边栏"> <section class="sidebar-card article-author-card"> <img class="profile-avatar" src="https://free.picui.cn/free/2025/08/10/689845496a283.png" alt="biss" loading="lazy"> <div class="profile-name">biss</div> <div class="article-author-meta"> <span>2025/08/12</span> <a href="/categories/site-notes/">建站手札</a> </div> </section> <section class="sidebar-card article-tags-card"> <h2> <span aria-hidden="true">#</span>
文章标签
<em>1</em> </h2> <div class="article-tag-list" aria-label="文章标签"> <a href="/tags/%E7%BD%91%E7%AB%99/">#网站</a> </div> </section> <div class="article-sticky-sidebar"> <section class="sidebar-card toc-card"> <h2> <span aria-hidden="true"></span>
文章目录
<em>8</em> </h2> <nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree> <div class="toc-group is-open" data-toc-group data-heading-id="api推荐"> <a class="toc-link toc-link-main" href="#api推荐" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>API推荐</span> </a> </div><div class="toc-group" data-toc-group data-heading-id="hexo适配"> <a class="toc-link toc-link-main" href="#hexo适配" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>Hexo适配</span> </a> <div class="toc-children" data-toc-children> <a class="toc-link toc-link-child toc-depth-2" href="#添加配置" data-toc-link> <span>添加配置</span> </a><a class="toc-link toc-link-child toc-depth-2" href="#添加模板" data-toc-link> <span>添加模板</span> </a><a class="toc-link toc-link-child toc-depth-2" href="#添加样式" data-toc-link> <span>添加样式</span> </a><a class="toc-link toc-link-child toc-depth-2" href="#添加核心js" data-toc-link> <span>添加核心JS</span> </a><a class="toc-link toc-link-child toc-depth-3" href="#打字机" data-toc-link> <span>打字机</span> </a><a class="toc-link toc-link-child toc-depth-3" href="#平滑" data-toc-link> <span>平滑</span> </a> </div> </div> </nav> </section> <section class="sidebar-card same-category-card"> <h2> <span aria-hidden="true"></span>
同类文章
<em>8</em> </h2> <div class="same-category-list"> <a href="/posts/8f3b9d21/"> <strong>网站迁移日志</strong> <time datetime="2026-06-19T16:45:00.000Z">2026/06/20</time> </a><a href="/posts/4c91a8f2/"> <strong>用 Umami 分享链接给网站信息卡提供统计数据</strong> <time datetime="2026-06-19T08:58:00.000Z">2026/06/19</time> </a><a href="/posts/7e921903/"> <strong>添加网站左上角菜单</strong> <time datetime="2026-02-10T07:11:14.000Z">2026/02/10</time> </a><a href="/posts/f287c563/"> <strong>添加typesense搜索</strong> <time datetime="2026-02-05T13:14:16.000Z">2026/02/05</time> </a><a href="/posts/b5601a7e/"> <strong>自定义字体</strong> <time datetime="2026-01-16T07:47:15.000Z">2026/01/16</time> </a><a href="/posts/5ed2f1e6/"> <strong>在侧边栏添加日历和倒计时</strong> <time datetime="2026-01-16T07:47:07.000Z">2026/01/16</time> </a><a href="/posts/3a26a97a/"> <strong>为博客添加CMS系统</strong> <time datetime="2025-08-25T22:03:00.000Z">2025/08/26</time> </a><a href="/posts/ce1ec3fe/"> <strong>使用GitHub推送Hexo到服务器</strong> <time datetime="2025-08-18T17:10:18.000Z">2025/08/19</time> </a> </div> </section> <section class="sidebar-card site-info-card" data-site-info-card data-stats-endpoint="https://blogumami.biss.click"> <h2><i class="fa-solid fa-chart-line" aria-hidden="true"></i> 网站信息</h2> <dl class="site-info-list"> <div> <dt>文章数目</dt> <dd>44</dd> </div><div> <dt>本站总字数</dt> <dd>36,128</dd> </div><div> <dt>本站访客数</dt> <dd data-stat-key="visitors">--</dd> </div><div> <dt>本站访问量</dt> <dd data-stat-key="pageviews">--</dd> </div><div> <dt>最后更新时间</dt> <dd>2026/06/20</dd> </div> </dl> </section> <script>
(() => {
const cacheDuration = 10 * 60 * 1000;
const formatter = new Intl.NumberFormat('en-US');
const cards = [...document.querySelectorAll('[data-site-info-card]')];
const endpoint = cards.find((card) => card.dataset.statsEndpoint)?.dataset.statsEndpoint;
if (!endpoint) return;
const cacheKey = `site-info-stats:${endpoint}`;
const updateCards = (stats) => {
const visitors = Number(stats?.visitors);
const pageviews = Number(stats?.pageviews);
cards.forEach((card) => {
const visitorsEl = card.querySelector('[data-stat-key="visitors"]');
const pageviewsEl = card.querySelector('[data-stat-key="pageviews"]');
if (visitorsEl && Number.isFinite(visitors)) visitorsEl.textContent = formatter.format(visitors);
if (pageviewsEl && Number.isFinite(pageviews)) pageviewsEl.textContent = formatter.format(pageviews);
});
};
try {
const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
if (cached?.time && Date.now() - cached.time < cacheDuration) {
updateCards(cached.data);
return;
}
} catch {
localStorage.removeItem(cacheKey);
}
fetch(endpoint, { headers: { Accept: 'application/json' } })
.then((response) => {
if (!response.ok) throw new Error(`Stats request failed: ${response.status}`);
return response.json();
})
.then((data) => {
localStorage.setItem(cacheKey, JSON.stringify({ time: Date.now(), data }));
updateCards(data);
})
.catch(() => {
});
})();
</script> </div> </aside> </section> </main> <footer class="site-footer" aria-label="站点页脚"> <div class="footer-inner"> <section class="footer-top" aria-label="站点信息"> <a class="footer-brand" href="/" aria-label="Bi's Blog 首页"> <img src="/images/Bi.ico" alt="" loading="lazy"> <span>Bi&#39;s Blog</span> </a> <p class="footer-slogan">好奇心转化为代码的空间。</p> </section> <section class="footer-contact" aria-label="联系方式"> <p>联系方式</p> <div class="footer-socials"> <a href="mailto:bishsh2006@gmail.com" aria-label="发送邮件" title="发送邮件"> <i class="fa-solid fa-envelope" aria-hidden="true"></i> </a><a href="/rss.xml" aria-label="订阅 RSS" title="订阅 RSS"> <i class="fa-solid fa-rss" aria-hidden="true"></i> </a><a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub" title="GitHub"> <i class="fa-brands fa-github" aria-hidden="true"></i> </a> </div> <nav class="footer-contact-links" aria-label="页脚链接"> <a href="/archives/">发展历程</a> <span aria-hidden="true">|</span><a href="/privacy/">隐私政策</a> </nav> </section> <section class="footer-bottom" aria-label="版权信息"> <div> <p>© 2025 - 2026 Bi&#39;s Blog</p> <p>blog.biss.click | 内容采用 CC BY-NC-SA 4.0 协议</p> </div> <div class="footer-meta"> <p>本站已运行:<span id="site-runtime">计算中...</span></p> <p> <a href="https://fastly.com/" target="_blank" rel="noreferrer">Fastly</a> <span aria-hidden="true">|</span><a href="https://astro.build/" target="_blank" rel="noreferrer">ASTRO</a> </p> </div> </section> </div> </footer> <aside class="hitokoto-card is-collapsed" aria-label="一言" data-hitokoto-card data-astro-cid-4cgfuthx> <div class="hitokoto-card__head" data-astro-cid-4cgfuthx> <button class="hitokoto-card__toggle" type="button" aria-label="展开一言" aria-expanded="false" title="一言" data-hitokoto-toggle data-astro-cid-4cgfuthx> <i class="fa-solid fa-quote-left" aria-hidden="true" data-astro-cid-4cgfuthx></i> <span data-astro-cid-4cgfuthx>一言</span> </button> <div class="hitokoto-card__actions" data-astro-cid-4cgfuthx> <button class="hitokoto-card__refresh" type="button" aria-label="换一句" title="换一句" data-hitokoto-refresh data-astro-cid-4cgfuthx> <i class="fa-solid fa-rotate" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> <button class="hitokoto-card__collapse" type="button" aria-label="收起一言" title="收起" data-hitokoto-collapse data-astro-cid-4cgfuthx> <i class="fa-solid fa-chevron-down" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> </div> </div> <a class="hitokoto-card__text" href="https://hitokoto.cn/" target="_blank" rel="noreferrer" data-hitokoto-text data-astro-cid-4cgfuthx>
获取中...
</a> <p class="hitokoto-card__from" data-hitokoto-from data-astro-cid-4cgfuthx>来自 Hitokoto</p> </aside> <script type="module">(()=>{const o=document.querySelector("[data-hitokoto-card]"),c=o?.querySelector("[data-hitokoto-toggle]"),s=o?.querySelector("[data-hitokoto-text]"),l=o?.querySelector("[data-hitokoto-from]"),g=o?.querySelector("[data-hitokoto-refresh]"),k=o?.querySelector("[data-hitokoto-collapse]");if(!(o instanceof HTMLElement)||!(c instanceof HTMLButtonElement)||!(s instanceof HTMLAnchorElement)||!(l instanceof HTMLElement))return;const y="https://yiyan.biss.click/api?encode=json",u="hitokoto-card-cache",d="hitokoto-card-expanded",x=360*60*1e3,f={sentence:"生活明朗,万物可爱。",source:"本地缓存",url:"https://hitokoto.cn/"},S=["/","/index.html"].includes(window.location.pathname),w=window.matchMedia("(max-width: 720px)");function E(){try{const t=localStorage.getItem(d);if(t==="true")return!0;if(t==="false")return!1}catch{}return null}function L(t){try{localStorage.setItem(d,String(t))}catch{}}function r(t,{persist:e=!1}={}){o.classList.toggle("is-collapsed",!t),c.setAttribute("aria-expanded",String(t)),c.setAttribute("aria-label",t?"收起一言":"展开一言"),e&&L(t)}function h(){try{const t=localStorage.getItem(u);if(!t)return null;const e=JSON.parse(t);return!e||typeof e.expiresAt!="number"||e.expiresAt<=Date.now()?null:e.data}catch{return null}}function H(t){try{localStorage.setItem(u,JSON.stringify({data:t,expiresAt:Date.now()+x}))}catch{}}function i(t){s.textContent=t.sentence,s.href=t.url||"https://hitokoto.cn/",l.textContent=t.source?`出自 ${t.source}`:"来自 Hitokoto"}async function p({force:t=!1}={}){if(!t){const e=h();if(e){i(e);return}}o.classList.add("is-loading"),s.textContent="获取中...";try{const e=await fetch(y);if(!e.ok)throw new Error(`Hitokoto request failed: ${e.status}`);const n=await e.json(),a=n.hitokoto||n.content||n.sentence||n.text,q=typeof a=="string"&&a.trim()?a.trim():f.sentence,A=typeof n.from=="string"&&n.from.trim()?n.from.trim():"Hitokoto",M=n.uuid?`https://hitokoto.cn/?uuid=${encodeURIComponent(n.uuid)}`:"https://hitokoto.cn/",m={sentence:q,source:A,url:M};H(m),i(m)}catch(e){console.error(e),i(h()||f)}finally{o.classList.remove("is-loading")}}const b=E();r(b??(S&&!w.matches)),c.addEventListener("click",()=>{r(o.classList.contains("is-collapsed"),{persist:!0})}),g?.addEventListener("click",t=>{t.stopPropagation(),r(!0,{persist:!0}),p({force:!0})}),k?.addEventListener("click",t=>{t.stopPropagation(),r(!1,{persist:!0})}),o.addEventListener("click",t=>{o.classList.contains("is-collapsed")&&(t.target instanceof Element&&t.target.closest("a, button")||r(!0,{persist:!0}))}),p()})();</script> <div class="custom-context-menu" data-context-menu role="menu" aria-hidden="true"> <div class="context-menu-nav" aria-label="页面导航"> <button type="button" data-context-action="back" aria-label="后退上一页" title="后退上一页"> <i class="fa-solid fa-arrow-left" aria-hidden="true"></i> </button> <button type="button" data-context-action="forward" aria-label="前进下一页" title="前进下一页"> <i class="fa-solid fa-arrow-right" aria-hidden="true"></i> </button> <button type="button" data-context-action="reload" aria-label="刷新页面" title="刷新页面"> <i class="fa-solid fa-rotate-right" aria-hidden="true"></i> </button> <a href="/" aria-label="返回首页" title="返回首页"> <i class="fa-solid fa-house" aria-hidden="true"></i> </a> </div> <div class="context-menu-group" data-selection-group hidden> <button type="button" data-context-action="copy-selection" data-selection-item> <i class="fa-regular fa-copy" aria-hidden="true"></i> <span>复制选中文字</span> </button> <button type="button" data-context-action="search-selection" data-selection-item> <i class="fa-brands fa-microsoft" aria-hidden="true"></i> <span>用 Bing 搜索</span> </button> <button type="button" data-context-action="quote-selection" data-selection-item data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>引用到评论区</span> </button> </div> <div class="context-menu-group" data-image-group hidden> <button type="button" data-context-action="copy-image"> <i class="fa-regular fa-image" aria-hidden="true"></i> <span>复制图片</span> </button> <button type="button" data-context-action="open-image"> <i class="fa-solid fa-up-right-from-square" aria-hidden="true"></i> <span>新页面打开图片</span> </button> <button type="button" data-context-action="copy-image-url"> <i class="fa-solid fa-link" aria-hidden="true"></i> <span>复制图片地址</span> </button> </div> <div class="context-menu-group"> <a href="/archives/"> <i class="fa-solid fa-box-archive" aria-hidden="true"></i> <span>时间线</span> </a> <a href="/categories/"> <i class="fa-solid fa-folder" aria-hidden="true"></i> <span>分类</span> </a> <a href="/tags/"> <i class="fa-solid fa-hashtag" aria-hidden="true"></i> <span>标签</span> </a> <button type="button" data-context-action="random-post"> <i class="fa-solid fa-shuffle" aria-hidden="true"></i> <span>&#x968F;&#x673A;&#x6587;&#x7AE0;</span> </button> <button type="button" data-context-action="comments" data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>跳转评论区</span> </button> <button type="button" data-context-action="print"> <i class="fa-solid fa-print" aria-hidden="true"></i> <span>打印页面</span> </button> <a href="/cc/"> <i class="fa-solid fa-circle-info" aria-hidden="true"></i> <span>网站版权</span> </a> </div> </div> <div class="context-menu-native-hint" data-context-native-hint role="note" aria-label="Ctrl 右键提示"> <kbd>Ctrl</kbd> + &#x53F3;&#x952E;&#xFF1A;&#x6D4F;&#x89C8;&#x5668;&#x539F;&#x751F;&#x83DC;&#x5355;
</div> <script>
(() => {
const menu = document.querySelector('[data-context-menu]');
if (!menu) return;
const nativeHint = document.querySelector('[data-context-native-hint]');
const selectionItems = [...menu.querySelectorAll('[data-selection-item]')];
const selectionGroup = menu.querySelector('[data-selection-group]');
const commentsItems = [...menu.querySelectorAll('[data-comments-item]')];
const imageGroup = menu.querySelector('[data-image-group]');
const focusableSelector = 'a[href], button:not([disabled])';
let lastSelectionText = '';
let lastImage = null;
let nativeHintTimer = 0;
const isNativeContextTarget = (target) =>
target instanceof Element &&
Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
const getCommentsTarget = () =>
document.querySelector('.twikoo-comments, #twikoo-wrap, #twikoo, .tk-comments-container');
const getSelectionText = () => String(window.getSelection?.() || '').trim();
const getImageTarget = (target) => {
if (!(target instanceof Element)) return null;
const image = target.closest('img');
return image instanceof HTMLImageElement ? image : null;
};
const getImageUrl = () => {
if (!lastImage) return '';
return lastImage.currentSrc || lastImage.src || lastImage.getAttribute('src') || '';
};
const copyText = async (text) => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.inset = '0 auto auto -9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
};
const setMenuState = () => {
const hasSelection = lastSelectionText.length > 0;
const hasComments = Boolean(getCommentsTarget());
const hasImage = Boolean(getImageUrl());
selectionItems.forEach((item) => {
item.hidden = !hasSelection;
});
if (selectionGroup) selectionGroup.hidden = !hasSelection;
commentsItems.forEach((item) => {
item.hidden = !hasComments || (item.hasAttribute('data-selection-item') && !hasSelection);
});
if (imageGroup) imageGroup.hidden = !hasImage;
};
const closeMenu = () => {
menu.classList.remove('is-open');
menu.setAttribute('aria-hidden', 'true');
};
const openMenu = (x, y) => {
setMenuState();
menu.classList.add('is-open');
menu.setAttribute('aria-hidden', 'false');
const rect = menu.getBoundingClientRect();
const gap = 10;
const left = Math.min(Math.max(gap, x), window.innerWidth - rect.width - gap);
const top = Math.min(Math.max(gap, y), window.innerHeight - rect.height - gap);
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
};
const showNativeHint = () => {
if (!nativeHint) return;
window.clearTimeout(nativeHintTimer);
nativeHint.classList.add('is-visible');
nativeHintTimer = window.setTimeout(() => {
nativeHint.classList.remove('is-visible');
}, 2600);
};
const jumpToComments = () => {
const target = getCommentsTarget();
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const quoteToComments = () => {
jumpToComments();
window.setTimeout(() => {
const textarea = document.querySelector('#twikoo textarea, .twikoo-comments textarea, .tk-input textarea');
if (!(textarea instanceof HTMLTextAreaElement) || !lastSelectionText) return;
const quote = lastSelectionText
.split(/\r?\n/)
.map((line) => `> ${line}`)
.join('\n');
const current = textarea.value.trim();
textarea.value = current ? `${current}\n\n${quote}\n` : `${quote}\n`;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
}, 500);
};
const searchSelection = () => {
if (!lastSelectionText) return;
const url = `https://www.bing.com/search?q=${encodeURIComponent(lastSelectionText)}`;
window.open(url, '_blank', 'noopener,noreferrer');
};
const copyImage = async () => {
const imageUrl = getImageUrl();
if (!imageUrl) return;
try {
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {
throw new Error('Image clipboard is not available.');
}
const response = await fetch(imageUrl, { mode: 'cors' });
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({ [blob.type || 'image/png']: blob }),
]);
} catch {
await copyText(new URL(imageUrl, window.location.href).toString());
}
};
const openImage = () => {
const imageUrl = getImageUrl();
if (!imageUrl) return;
window.open(new URL(imageUrl, window.location.href).toString(), '_blank', 'noopener,noreferrer');
};
document.addEventListener('contextmenu', (event) => {
if (event.ctrlKey) {
closeMenu();
nativeHint?.classList.remove('is-visible');
return;
}
if (isNativeContextTarget(event.target)) return;
lastSelectionText = getSelectionText();
lastImage = getImageTarget(event.target);
event.preventDefault();
openMenu(event.clientX, event.clientY);
showNativeHint();
});
document.addEventListener('click', (event) => {
const actionTarget = event.target instanceof Element ? event.target.closest('[data-context-action]') : null;
if (!actionTarget || !menu.contains(actionTarget)) {
closeMenu();
return;
}
const action = actionTarget.dataset.contextAction;
closeMenu();
if (action === 'back') history.back();
if (action === 'forward') history.forward();
if (action === 'reload') location.reload();
if (action === 'copy-selection' && lastSelectionText) copyText(lastSelectionText);
if (action === 'search-selection') searchSelection();
if (action === 'quote-selection') quoteToComments();
if (action === 'copy-image') copyImage();
if (action === 'open-image') openImage();
if (action === 'copy-image-url') copyText(new URL(getImageUrl(), window.location.href).toString());
if (action === 'random-post' && typeof window.randomPost === 'function') window.randomPost(actionTarget);
if (action === 'comments') jumpToComments();
if (action === 'print') window.print();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeMenu();
if (event.key === 'Tab' && menu.classList.contains('is-open')) {
const items = [...menu.querySelectorAll(focusableSelector)].filter((item) => !item.closest('[hidden]'));
if (items.length === 0) return;
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
});
window.addEventListener('scroll', closeMenu, { passive: true });
window.addEventListener('resize', closeMenu);
})();
</script> <aside class="pwa-toast" data-pwa-toast hidden role="status" aria-live="polite"> <div class="pwa-toast-icon" aria-hidden="true"><i class="fa-solid fa-download"></i></div> <div class="pwa-toast-copy"> <strong data-pwa-title>PWA 提示</strong> <p data-pwa-message></p> </div> <div class="pwa-toast-actions"> <button class="pwa-toast-primary" type="button" data-pwa-primary>确定</button> <button class="pwa-toast-close" type="button" data-pwa-close aria-label="关闭提示">稍后</button> </div> </aside> <script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script> <script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script> <script src="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script> <script src="/js/random.js"></script> <script src="/js/search/typesense-search.js"></script> <script src="/js/external-links.js"></script> <script src="/js/pwa.js"></script> <script>
(() => {
const nav = document.querySelector('.nav');
const toggle = document.querySelector('[data-nav-toggle]');
const menu = document.querySelector('[data-nav-menu]');
const backdrop = document.querySelector('[data-nav-backdrop]');
if (!nav || !toggle || !menu) return;
function setMenu(open) {
nav.classList.toggle('is-open', open);
toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', open ? '关闭菜单' : '打开菜单');
menu.setAttribute('aria-hidden', String(!open && window.innerWidth <= 720));
document.body.classList.toggle('has-nav-drawer', open);
}
toggle.addEventListener('click', () => {
setMenu(!nav.classList.contains('is-open'));
});
backdrop?.addEventListener('click', () => setMenu(false));
menu.addEventListener('click', (event) => {
if (event.target instanceof Element && event.target.closest('a')) setMenu(false);
});
document.addEventListener('click', (event) => {
if (!nav.classList.contains('is-open')) return;
if (event.target instanceof Node && nav.contains(event.target)) return;
setMenu(false);
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && nav.classList.contains('is-open')) setMenu(false);
});
window.addEventListener('resize', () => {
if (window.innerWidth > 720) setMenu(false);
else menu.setAttribute('aria-hidden', String(!nav.classList.contains('is-open')));
});
menu.setAttribute('aria-hidden', String(window.innerWidth <= 720));
})();
(() => {
const modal = document.querySelector('[data-console-modal]');
const panel = modal?.querySelector('.control-console-panel');
const openButtons = document.querySelectorAll('[data-console-open]');
const closeButtons = modal?.querySelectorAll('[data-console-close]') || [];
if (!modal || !panel || openButtons.length === 0) return;
function setConsole(open) {
modal.classList.toggle('is-open', open);
modal.setAttribute('aria-hidden', String(!open));
document.body.classList.toggle('has-control-console', open);
if (open) panel.focus();
}
openButtons.forEach((button) => {
button.addEventListener('click', () => setConsole(true));
});
closeButtons.forEach((button) => {
button.addEventListener('click', () => setConsole(false));
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && modal.classList.contains('is-open')) setConsole(false);
});
})();
(() => {
const storageKey = 'site-theme';
const root = document.documentElement;
const button = document.querySelector('[data-theme-toggle]');
const media = window.matchMedia('(prefers-color-scheme: dark)');
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
const getSavedTheme = () => {
try {
const theme = localStorage.getItem(storageKey);
return theme === 'dark' || theme === 'light' ? theme : null;
} catch {
return null;
}
};
const setSavedTheme = (theme) => {
try {
if (theme === getSystemTheme()) localStorage.removeItem(storageKey);
else localStorage.setItem(storageKey, theme);
} catch {
// localStorage can be unavailable in strict privacy modes.
}
};
const getTheme = () => getSavedTheme() || getSystemTheme();
function applyTheme(theme) {
root.dataset.theme = theme;
root.dataset.themeMode = getSavedTheme() ? 'manual' : 'auto';
button?.setAttribute('aria-label', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
button?.setAttribute(
'title',
getSavedTheme()
? (theme === 'dark' ? '切换到日间模式' : '切换到夜间模式')
: `跟随系统:${theme === 'dark' ? '夜间' : '日间'}模式`
);
}
applyTheme(getTheme());
button?.addEventListener('click', () => {
const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark';
setSavedTheme(nextTheme);
applyTheme(nextTheme);
});
const handleSystemThemeChange = () => {
if (!getSavedTheme()) applyTheme(getSystemTheme());
};
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', handleSystemThemeChange);
} else if (typeof media.addListener === 'function') {
media.addListener(handleSystemThemeChange);
}
})();
(() => {
const runtime = document.getElementById('site-runtime');
if (!runtime) return;
const start = new Date(window.SITE_CONFIG.site.startTime).getTime();
const pad = (value) => String(value).padStart(2, '0');
function updateRuntime() {
const diff = Math.max(0, Date.now() - start);
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff / 3600000) % 24);
const minutes = Math.floor((diff / 60000) % 60);
const seconds = Math.floor((diff / 1000) % 60);
runtime.textContent = `${days}${pad(hours)}${pad(minutes)}${pad(seconds)} 秒`;
}
updateRuntime();
window.setInterval(updateRuntime, 1000);
})();
</script> </body> </html> <script>(function(){const postSlug = "41b9aff7";
(() => {
const article = document.querySelector('.post-article');
const progressBar = document.querySelector('[data-reading-progress]');
const resume = document.querySelector('[data-reading-resume]');
const resumePercent = document.querySelector('[data-reading-resume-percent]');
const continueButton = document.querySelector('[data-reading-continue]');
const restartButton = document.querySelector('[data-reading-restart]');
if (!article || !progressBar || !resume) return;
const storageKey = `reading-progress:${postSlug}`;
let saved = null;
let canPersist = true;
let frame = 0;
try {
saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
} catch {
saved = null;
}
const articleMetrics = () => {
const top = article.getBoundingClientRect().top + window.scrollY;
const distance = Math.max(1, article.offsetHeight - window.innerHeight);
return { top, distance };
};
const getProgress = () => {
const { top, distance } = articleMetrics();
return Math.min(1, Math.max(0, (window.scrollY - top) / distance));
};
const persist = (progress) => {
if (!canPersist) return;
try {
if (progress >= 0.97) localStorage.removeItem(storageKey);
else if (progress >= 0.02) localStorage.setItem(storageKey, JSON.stringify({ progress, savedAt: Date.now() }));
} catch {
// Storage can be unavailable in strict privacy modes.
}
};
const update = () => {
frame = 0;
const progress = getProgress();
progressBar.style.transform = `scaleX(${progress})`;
persist(progress);
};
const requestUpdate = () => {
if (!frame) frame = window.requestAnimationFrame(update);
};
const closeResume = () => {
resume.hidden = true;
canPersist = true;
};
const savedIsFresh = saved && Date.now() - Number(saved.savedAt) < 90 * 86400000;
const savedProgress = Number(saved?.progress);
if (savedIsFresh && savedProgress >= 0.05 && savedProgress < 0.97) {
canPersist = false;
if (resumePercent) resumePercent.textContent = `${Math.round(savedProgress * 100)}%`;
window.setTimeout(() => { resume.hidden = false; }, 350);
}
continueButton?.addEventListener('click', () => {
closeResume();
const { top, distance } = articleMetrics();
window.scrollTo({ top: top + distance * savedProgress, behavior: 'smooth' });
});
restartButton?.addEventListener('click', () => {
try { localStorage.removeItem(storageKey); } catch {}
closeResume();
window.scrollTo({ top: Math.max(0, articleMetrics().top - 90), behavior: 'smooth' });
});
update();
window.addEventListener('scroll', requestUpdate, { passive: true });
window.addEventListener('resize', requestUpdate);
window.addEventListener('pagehide', () => persist(getProgress()));
})();
})();</script> <script>
(() => {
document.querySelectorAll('.article-outdated-note[data-freshness-at]').forEach((note) => {
const freshnessAt = new Date(note.dataset.freshnessAt || '');
const hasNewVersion = note.dataset.hasNewVersion === 'true';
if (Number.isNaN(freshnessAt.valueOf())) return;
const threshold = new Date();
threshold.setFullYear(threshold.getFullYear() - 1);
note.hidden = !hasNewVersion && freshnessAt >= threshold;
});
})();
</script> <script>
(() => {
const prose = document.querySelector('.prose[data-toc]');
if (!prose) return;
let toc = [];
try {
toc = JSON.parse(prose.dataset.toc || '[]');
} catch {
toc = [];
}
prose.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
const item = toc[index];
if (item?.id && !heading.id) heading.id = item.id;
});
const tocTree = document.querySelector('[data-toc-tree]');
if (tocTree && toc.length > 0) {
const links = [...tocTree.querySelectorAll('[data-toc-link]')];
const groups = [...tocTree.querySelectorAll('[data-toc-group]')];
const headings = toc
.map((item) => document.getElementById(item.id))
.filter(Boolean);
const setActiveToc = (id) => {
links.forEach((link) => {
const isActive = link.getAttribute('href') === `#${id}`;
link.classList.toggle('is-active', isActive);
if (isActive) link.setAttribute('aria-current', 'location');
else link.removeAttribute('aria-current');
});
const activeLink = tocTree.querySelector(`[data-toc-link][href="#${CSS.escape(id)}"]`);
const activeGroup = activeLink?.closest('[data-toc-group]');
groups.forEach((group) => group.classList.toggle('is-open', group === activeGroup));
};
const updateToc = () => {
const anchorOffset = 132;
let activeId = headings[0]?.id;
headings.forEach((heading) => {
if (heading.getBoundingClientRect().top <= anchorOffset) activeId = heading.id;
});
if (activeId) setActiveToc(activeId);
};
tocTree.addEventListener('click', (event) => {
const link = event.target.closest?.('[data-toc-link]');
if (!link) return;
const group = link.closest('[data-toc-group]');
groups.forEach((item) => item.classList.toggle('is-open', item === group));
});
updateToc();
window.addEventListener('scroll', updateToc, { passive: true });
window.addEventListener('resize', updateToc);
}
})();
</script> <script>
(() => {
const prose = document.querySelector('.post-article .prose');
if (!prose) return;
const collapsedLineCount = 8;
const copyLabels = {
idle: '复制',
done: '已复制',
error: '复制失败',
};
const languageNames = {
bash: 'Bash',
c: 'C',
cpp: 'C++',
cs: 'C#',
css: 'CSS',
html: 'HTML',
js: 'JavaScript',
json: 'JSON',
jsx: 'JSX',
md: 'Markdown',
powershell: 'PowerShell',
py: 'Python',
shell: 'Shell',
sh: 'Shell',
ts: 'TypeScript',
tsx: 'TSX',
txt: 'Text',
yaml: 'YAML',
yml: 'YAML',
};
function getCodeLanguage(pre, code) {
const rawLanguage =
pre.dataset.language ||
code.dataset.language ||
[...pre.classList, ...code.classList]
.find((className) => className.startsWith('language-'))
?.replace(/^language-/, '') ||
'';
const normalized = rawLanguage.trim().toLowerCase();
return languageNames[normalized] || normalized.toUpperCase() || 'Code';
}
function getCodeText(code, lines) {
if (lines.length > 0) return lines.map((line) => line.textContent || '').join('\n');
return code.textContent || '';
}
prose.querySelectorAll('img').forEach((image) => {
if (image.closest('a, .article-header-cover')) return;
const src = image.currentSrc || image.getAttribute('src');
if (!src) return;
const link = document.createElement('a');
link.className = 'fancybox';
link.href = src;
link.dataset.fancybox = 'article-gallery';
link.dataset.caption = image.getAttribute('alt') || '';
image.parentNode?.insertBefore(link, image);
link.appendChild(image);
});
prose.querySelectorAll('table').forEach((table) => {
if (table.closest('.table-scroll')) return;
const wrapper = document.createElement('div');
wrapper.className = 'table-scroll';
table.parentNode?.insertBefore(wrapper, table);
wrapper.appendChild(table);
});
prose.querySelectorAll('.butterfly-hide-inline').forEach((item) => {
const button = item.querySelector('.butterfly-hide-toggle');
if (!button) return;
button.addEventListener('click', () => {
item.classList.toggle('is-open');
});
});
if (window.Fancybox) {
window.Fancybox.bind('[data-fancybox="article-gallery"]', {
compact: false,
dragToClose: true,
Images: {
zoom: true,
},
});
}
async function copyText(text) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
prose.querySelectorAll('pre').forEach((pre) => {
if (pre.closest('.code-block')) return;
const code = pre.querySelector('code');
if (!code) return;
const wrapper = document.createElement('div');
wrapper.className = 'code-block';
pre.parentNode?.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const header = document.createElement('div');
header.className = 'code-block-header';
const language = document.createElement('span');
language.className = 'code-block-language';
language.textContent = getCodeLanguage(pre, code);
const toolbar = document.createElement('div');
toolbar.className = 'code-block-toolbar';
const copyButton = document.createElement('button');
copyButton.className = 'code-block-copy';
copyButton.type = 'button';
copyButton.textContent = copyLabels.idle;
copyButton.setAttribute('aria-label', '复制代码');
const toggleButton = document.createElement('button');
toggleButton.className = 'code-block-toggle';
toggleButton.type = 'button';
toggleButton.textContent = '展开';
toggleButton.setAttribute('aria-expanded', 'false');
const expandCue = document.createElement('button');
expandCue.className = 'code-block-expand-cue';
expandCue.type = 'button';
expandCue.setAttribute('aria-label', 'Expand code block');
expandCue.setAttribute('aria-expanded', 'false');
expandCue.innerHTML = '<span aria-hidden="true"></span>';
toolbar.append(copyButton, toggleButton);
header.append(language, toolbar);
wrapper.appendChild(header);
wrapper.appendChild(expandCue);
const codeLines = [...code.querySelectorAll(':scope > .line')];
if (codeLines.length > 0) {
wrapper.classList.add('has-line-numbers');
wrapper.style.setProperty('--line-number-width', `${String(codeLines.length).length + 1}ch`);
codeLines.forEach((line, index) => {
line.dataset.line = String(index + 1);
});
}
const lineCount = codeLines.length || (code.textContent || '').split(/\r?\n/).length;
if (lineCount > collapsedLineCount) wrapper.classList.add('is-collapsible');
copyButton.addEventListener('click', async () => {
try {
await copyText(getCodeText(code, codeLines));
copyButton.textContent = copyLabels.done;
window.setTimeout(() => {
copyButton.textContent = copyLabels.idle;
}, 1400);
} catch {
copyButton.textContent = copyLabels.error;
window.setTimeout(() => {
copyButton.textContent = copyLabels.idle;
}, 1600);
}
});
function toggleExpanded() {
const isExpanded = wrapper.classList.toggle('is-expanded');
toggleButton.textContent = isExpanded ? '收起' : '展开';
toggleButton.setAttribute('aria-expanded', String(isExpanded));
expandCue.setAttribute('aria-expanded', String(isExpanded));
}
toggleButton.addEventListener('click', toggleExpanded);
expandCue.addEventListener('click', toggleExpanded);
});
})();
</script>