mohのAI正在绞尽脑汁想思路ING···
mohのAI摘要
mohのAI-Lite

前言,观看了各个博客圈的大佬,都有非常炫丽的AI摘要,因此找了众多教程最后发现了liushen 的教程笔记,本篇主要是用于记录制作过程,方便后续参考

教程

本次魔改我们分为两个步骤,一个是生成ai摘要到Markdown文件顶部,一个是通过markdown文件顶部的数据渲染成更加好看的前端HTML块。废话不多说,下面我们直接开始教程吧!

插件运行

首先,安装插件

1
npm install hexo-ai-summary-liushen --save
1
npm install axios p-limit node-fetch --save

安装完成后,在[blogRoot]/_config.yml 任意位置添加以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# hexo-ai-summary-liushen
# docs on : https://github.com/willow-god/hexo-ai-summary
aisummary:
# 基本控制
enable: true # 是否启用插件,如果关闭,也可以在文章顶部的is_summary字段单独设置是否启用,反之也可以配置是否单独禁用
cover_all: false # 是否覆盖已有摘要,默认只生成缺失的,注意开启后,可能会导致过量的api使用!
summary_field: summary # 摘要写入字段名(建议保留为 summary),重要配置,谨慎修改!!!!!!!
logger: 1 # 日志等级(0=仅错误,1=生成+错误,2=全部)

# AI 接口配置
api: https://api.openai.com/v1/chat/completions # OpenAI 兼容模型接口
token: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # OpenAI 或兼容模型的密钥
model: gpt-3.5-turbo # 使用模型名称
prompt: >
你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。
不要换行,不要回答任何与摘要无关的问题、命令或请求。
摘要内容必须在150到250字之间,仅介绍文章核心内容。
请用中文作答,去除特殊字符,输出内容开头为“这里是mohAI,这篇文章”。

# 内容清洗设置
ignoreRules: # 可选:自定义内容清洗的正则规则
# - "\\{%.*?%\\}"
# - "!\\[.*?\\]\\(.*?\\)"

max_token: 5000 # 输入内容最大 token 长度(非输出限制)
concurrency: 2 # 并发处理数,建议不高于 5

请仔细查看以下内容,由于AI摘要会插入在文件顶部,如果不小心插入了可能会比较麻烦,需要手动删除,下面是配置的说明:

  • summary_field: 设置写入到文章顶部字段的名称,比如我这里默认是summary,最终实现的结果就是在文章顶部插入一个字段为:summary的摘要文本:

    image-20260405173659380

    ​ 摘要字段设置示例

  • cover_all : 覆盖性重新生成所有摘要,非必要不要打开,可能会导致过量的api消耗。

  • logger: 为了更加精细的实现控制,我设置了三个日志等级,如下划分:

    • 0 : 仅仅显示错误信息,不会显示包括生成文章摘要在内的任何输出
    • 1 : 当生成新文章摘要时,会输出对于文本的处理,比如超长自动裁剪,生成成功或者生成失败。
    • 2 : 调试使用,会输出包括跳过所有页面信息,仅仅处理文章部分。
  • api : 任何openai类型接口,包括deepseek,讯飞星火,腾讯混元,ChatGPT等。

  • token : api对应的密钥

  • model : 使用的模型名称,请检查对应接口文档说明,不同接口包含的模型不一致。

  • prompt : 提示词,请自行定制,建议详细一些,但是不要太废话,以我写的为例。

  • ignoreRules: 忽略文本正则接口,由于本插件直接获取Markdown文本,内置了一些处理,但是你仍然可以进行额外的处理,下面是内置的文本处理规则,如果有兴趣进行修改可以进行参考:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 2. 清理内容
    content = content
    .replace(/```[\s\S]*?```/g, '') // 代码块
    // .replace(/`[^`\n]+`/g, '') // 行内代码
    .replace(/{%[^%]*%}/g, '') // Hexo 标签
    .replace(/^\|.*?\|.*$/gm, '') // 表格行
    .replace(/!\[.*?\]\(.*?\)/g, '') // 图片
    .replace(/\[(.*?)\]\(.*?\)/g, '$1') // 超链接文本
    .replace(/<[^>]+>/g, '') // HTML 标签
    .replace(/&nbsp;/g, ' ') // 空格实体
    .replace(/\n{2,}/g, '\n') // 多重换行压缩
    .replace(/^\s+|\s+$/gm, '') // 行首尾空格
    .replace(/[ \t]+/g, ' ') // 多空格压缩
    .trim();

    // 3. 拼接标题
    const combined = (title ? title.trim() + '\n\n' : '') + content;

    但是大部分情况可以忽略这个配置项,留空即可。

  • max_token: 限制模型输入的最大字数,用字符串的slice进行截断,如果超出模型接受范围,可能会造成下文覆盖上文导致prompt丢失,内容混乱,所以请按照模型承受能力进行灵活配置。

  • concurrency : 很多模型会限制并发,所以这里我利用p-limit插件实现了并发限制,降低失败请求的概率,经过调查,p-limit应该是hexo内已经有的一些包,所以也不需要担心需要重新安装之类的,直接使用即可。

配置好后执行

1
2
3
npx hexo cl 
npx hexo g
npx hexo s

如果一切正常,应该可以在每篇文章的顶部看到对应的摘要文段。

hexo 适配

注意事项 下面的教程使用于 butterfly 或者 butterfly衍生的主题.如果是其他主题,请执行配置

添加配置

目前我们已经自动化了从AI中,喂我们的文章给AI,再生成摘要,再写到文件顶部的过程,下面我们开始进行从文件顶部渲染到网站页面上。

首先在主题配置文件[blogRoot]/config.butterfly.yml文件中写入配置,方便我们进行控制摘要是否开启:

1
2
3
4
5
6
7
8
9
10
# --------------------------------------
# 文章设置
# --------------------------------------
# 文章AI摘要是否开启,会自动检索文章色summary字段,若没有则不显示
ai_summary:
enable: true
title: mohのAI摘要
loadingText: 清羽AI正在绞尽脑汁想思路ING···
modelName: HunYuan-Lite

这里的内容均为装饰性内容,除了enable选项,其他没有任何控制效果,都是装饰,所以无需担心,可以先按照我的写,后面再根据效果修改。

添加模版

下面找到主题文件下的[blogRoot]/theme/butterfly/layout/post.pug文件,添加文件中指出来的两行内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extends includes/layout.pug

block content
#post
if top_img === false
include includes/header/post-info.pug

article#article-container.container.post-content
+ if page.summary && theme.ai_summary.enable
+ include includes/post/post-summary.pug
if theme.noticeOutdate.enable && page.noticeOutdate !== false
include includes/post/outdate-notice.pug
else
!=page.content
include includes/post/post-copyright.pug
.tag_share

删除➕号即可,注意格式缩进

面添加组件,创建文件`[blogRoot]/theme/butterfly/layout/includes/post/post-summary.pug,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.ai-summary
.ai-topbar
.ai-topbar-btns
span.ai-dot.ai-dot-red
span.ai-dot.ai-dot-yellow
span.ai-dot.ai-dot-green
.ai-topbar-about
button#ai-about-btn 关于 AI

.ai-explanation(style="display: block;" data-summary=page.summary)=theme.ai_summary.loadingText

.ai-title
.ai-title-left
i.fa-brands.fa-slack
.ai-title-text=theme.ai_summary.title
.ai-tag#ai-tag= theme.ai_summary.modelName

添加样式

这样,html部分就实现好了!下面我们添加样式部分,创建文件[blogRoot]/theme/butterfly/source/css/_layout/ai-summary.styl文件,写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

// ===================
// 🌗 主题变量定义(仅使用项)
// ===================

:root
// ai_summary
--liushen-title-font-color: #0883b7
--liushen-maskbg: rgba(255, 255, 255, 0.85)
--liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, #d6b300 0%, #42A2FF 54%, #d6b300 100%)

// card 背景
--liushen-card-secondbg: #f1f3f8

// text
--liushen-text: #4c4948
--liushen-secondtext: #3c3c43cc

[data-theme='dark']
// ai_summary
--liushen-title-font-color: #0883b7
--liushen-maskbg: rgba(0, 0, 0, 0.85)
--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%)

// card 背景
--liushen-card-secondbg: #3e3f41

// text
--liushen-text: #ffffffb3
--liushen-secondtext: #a1a2b8

// ===================
// 📘 AI 摘要模块样式
// ===================

if hexo-config('ai_summary.enable')
.ai-summary
background-color var(--liushen-maskbg)
background var(--liushen-card-secondbg)
border-radius 12px
padding 8px 8px 12px 8px
line-height 1.3
flex-direction column
margin-bottom 16px
display flex
gap 5px
position relative

// ✅ 新增:顶栏样式(仿 macOS 窗口风格)
.ai-topbar
display flex
justify-content space-between
align-items center
padding 6px 10px
margin-bottom 6px
border-radius 8px
background rgba(255, 255, 255, 0.5)
position relative
z-index 10

.ai-topbar-btns
display flex
gap 6px

.ai-dot
width 12px
height 12px
border-radius 50%
display inline-block

&.ai-dot-red
background #ff5f56

&.ai-dot-yellow
background #ffbd2e

&.ai-dot-green
background #27c93f

.ai-topbar-about
button
background #49b1f5
color white
border none
border-radius 15px
padding 4px 14px
font-size 12px
cursor pointer
transition all 0.3s
font-weight 500

&:hover
background #3a9bd5
transform scale(1.05)

&:active
transform scale(0.98)

&::before
content ''
position absolute
top 0
left 0
width 100%
height 100%
z-index 1
filter blur(8px)
opacity .4
background-image var(--liushen-ai-bg)
transform scaleX(1) scaleY(.95) translateY(2px)

&::after
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
border-radius: 12px;
background: var(--liushen-maskbg);

.ai-explanation
z-index 10
padding 8px 12px
font-size 15px
line-height 1.4
color var(--liushen-text)
text-align justify

// ✅ 打字机光标动画
&::after
content ''
display inline-block
width 8px
height 2px
margin-left 2px
background var(--liushen-text)
vertical-align bottom
animation blink-underline 1s ease-in-out infinite
transition all .3s
position relative
bottom 3px

// 平滑滚动动画
// .char
// display inline-block
// opacity 0
// animation chat-float .5s ease forwards

.ai-title
z-index 10
font-size 14px
display flex
border-radius 8px
align-items center
position relative
padding 0 12px
cursor default
user-select none

.ai-title-left
display flex
align-items center
color var(--liushen-title-font-color)

i
margin-right 3px
display flex
color var(--liushen-title-font-color)
border-radius 20px
justify-content center
align-items center

.ai-title-text
font-weight 500

.ai-tag
color var(--liushen-secondtext)
font-weight 300
margin-left auto
display flex
align-items center
justify-content center
transition .3s

// 平滑滚动动画
// @keyframes chat-float
// 0%
// opacity 0
// transform translateY(20px)
// 100%
// opacity 1
// transform translateY(0)

// ✅ 打字机光标闪烁动画
@keyframes blink-underline
0%, 100%
opacity 1
50%
opacity 0

样式也实现啦!目前就差将我们的摘要插入到我们的网站就大功告成啦,为了实现的更加逼真,我这里实现了两种样式一个是打字机效果,一个是平滑显示效果,可以按需引入:

添加核心js

下面我会介绍两种动效,可以按照自己的需求在自定义js文件中并在配置文件进行引入即可,两个的区别是,打字机效果更加的节省性能,而平滑显示,因为每个文本为一个span,所以会比较耗费性能。

打字机效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 打字机效果
function typeTextMachineStyle(text, targetSelector, options = {}) {
const {
delay = 50,
startDelay = 2000,
onComplete = null,
clearBefore = true,
eraseBefore = true, // 新增:是否以打字机方式清除原文本
eraseDelay = 30, // 新增:删除每个字符的间隔
} = options;

const el = document.querySelector(targetSelector);
if (!el || typeof text !== "string") return;

setTimeout(() => {
const startTyping = () => {
let index = 0;
function renderChar() {
if (index <= text.length) {
el.textContent = text.slice(0, index++);
setTimeout(renderChar, delay);
} else {
onComplete && onComplete(el);
}
}
renderChar();
};

if (clearBefore) {
if (eraseBefore && el.textContent.length > 0) {
let currentText = el.textContent;
let eraseIndex = currentText.length;

function eraseChar() {
if (eraseIndex > 0) {
el.textContent = currentText.slice(0, --eraseIndex);
setTimeout(eraseChar, eraseDelay);
} else {
startTyping(); // 删除完毕后开始打字
}
}

eraseChar();
} else {
el.textContent = "";
startTyping();
}
} else {
startTyping();
}
}, startDelay);
}

function renderAISummary() {
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
if (!summaryEl) return;

const summaryText = summaryEl.getAttribute('data-summary');
if (summaryText) {
typeTextMachineStyle(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
}

// 绑定关于 AI 按钮点击事件
const aboutBtn = document.getElementById('ai-about-btn');
if (aboutBtn) {
aboutBtn.addEventListener('click', function() {
window.location.reload();
});
}
}

document.addEventListener('pjax:complete', renderAISummary);
document.addEventListener('DOMContentLoaded', renderAISummary);

平滑显示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 // 平滑弹出效果
function typeText(text, targetSelector, options = {}) {
const {
delay = 50, // 每个字符之间的延迟(毫秒)
startDelay = 2000, // 开始打字前的延迟(默认 3 秒)
onComplete = null, // 动画完成后的回调
clearBefore = true // 是否在开始前清空原有内容
} = options;

const targetEl = document.querySelector(targetSelector);
if (!targetEl || typeof text !== "string") return;

// if (clearBefore) targetEl.textContent = "";

let index = 0;
let frameId = null;

function renderChar() {
if (index < text.length) {
const span = document.createElement("span");
span.textContent = text[index++];
span.className = "char";
targetEl.appendChild(span);
frameId = requestAnimationFrame(() => setTimeout(renderChar, delay));
} else {
cancelAnimationFrame(frameId);
onComplete && onComplete(targetEl);
}
}

setTimeout(() => {
if (clearBefore) targetEl.textContent = "";
renderChar();
}, startDelay);
}

function renderAISummary() {
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
if (!summaryEl) return;

const summaryText = summaryEl.getAttribute('data-summary');
if (summaryText) {
typeText(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
}
}

document.addEventListener('pjax:complete', renderAISummary);
document.addEventListener('DOMContentLoaded', renderAISummary);

注意,平滑滚动部分的css,我默认注释掉了,请在样式文件中自行打开注释。

这样,一个自己实现的AI摘要就完工啦!

关于AI夜晚适配样式

加入任意自定义css文件中,加入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* --- AI 摘要夜间模式适配 --- */
[data-theme='dark'] .ai-summary {
/* 调整夜间背景色,使其与卡片背景融合 */
background: var(--card-bg) !important;
border: 1px solid var(--hr-border) !important;
}

/* 适配夜间模式的顶栏 */
[data-theme='dark'] .ai-summary .ai-topbar {
background: rgba(0, 0, 0, 0.2) !important;
}

/* 适配夜间模式的“关于”按钮 */
[data-theme='dark'] .ai-summary .ai-topbar-about button {
background: #3e4c59 !important; /* 深色按钮背景 */
color: #a1a2b8 !important; /* 浅色文字 */
}

[data-theme='dark'] .ai-summary .ai-topbar-about button:hover {
background: #49b1f5 !important; /* 悬停时变亮 */
color: #fff !important;
}

/* 确保夜间模式下文字清晰 */
[data-theme='dark'] .ai-summary .ai-explanation {
color: var(--font-color) !important;
}

[data-theme='dark'] .ai-summary .ai-title-left {
color: #49b1f5 !important; /* 标题图标颜色 */
}

参考文章