本地实现 HEXO 文章 AI 摘要

木偶AI正在绞尽脑汁想思路ING···
木偶のAI摘要
DeepSeek-Chat

生成摘要

安装插件

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

配置插件

hexo_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
# 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字之间,仅介绍文章核心内容。
请用中文作答,去除特殊字符,输出内容开头为“这里是木偶のAI,这篇文章”。

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

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

适配样式

主题配置

在主题的 _config.anzhiyu.yml 中添加以下配置:

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

添加模板

在主题的 themes/anzhiyu/layout/post.pug 中添加以下代码:

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
extends includes/layout.pug

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

article#article-container.post-content
+ if page.summary && theme.ai_summary.enable
+ include includes/post/post-summary.pug
!=page.content
include includes/post/post-copyright.pug
.tag_share
if (page.tags.length > 0 && theme.post_meta.post.tags)
.post-meta__tag-list
each item, index in page.tags.data
a(href=url_for(item.path)).post-meta__tags #[=item.name]
include includes/third-party/share/index.pug

if theme.reward.enable && theme.reward.QR_code
!=partial('includes/post/reward', {}, {cache: true})

//- ad
if theme.ad && theme.ad.post
.ads-wrap!=theme.ad.post

if theme.post_pagination
include includes/pagination.pug
if theme.related_post && theme.related_post.enable
!= related_posts(page,site.posts)

if page.comments !== false && theme.comments.use
- var commentsJsLoad = true
!=partial('includes/third-party/comments/index', {}, {cache: true})

在主题的 themes/anzhiyu/layout/includes/post/post-summary.pug 中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.ai-summary
.ai-head
.ai-head-left
.ai-circle.ai-circle-1
.ai-circle.ai-circle-2
.ai-circle.ai-circle-3
.ai-head-right
a(href=url_for(theme.ai_summary.modelUrl) target="_blank" title=theme.ai_summary.modelName class="ai-about-ai") 关于 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

添加样式

在主题的 themes/anzhiyu/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
// ===================
// 🌗 主题变量定义(仅使用项)
// ===================

: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

&::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 文件中选择一个引入即可

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
// 打字机效果
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"); // 如果需要切换,在这里调用另一个函数即可
}
}

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