慕雪的小助手正在绞尽脑汁···
慕雪小助手的总结
LongCat-Flash-Chat

参考大佬的教程,为自己的博客站点加上了离线的AI摘要。

1. 写在前面

之前逛个个大佬的个人博客的时候,发现有不少大佬都给自己的博客文章开头加上了一个AI摘要的功能。我也想过类似的功能,但是当时以为这些AI摘要都是实时请求的,会太消耗Token,于是就没有处理了。

今天心血来潮又去搜了一下相关的教程,找到了LiuShen大佬fork制作的一份只需要hexo的front-matter就能在前端显示AI摘要的插件。这个插件就比较符合我的需求了,因为我不想要一个实时的AI请求,离线在本地给hexo文章的front-matter加上AI摘要,已经足够了。

参考博客:https://blog.liushen.fun/posts/40702a0d/
仓库开源地址:https://github.com/willow-god/hexo-ai-summary

大佬用的也是butterfly主题,所以针对主题的修改是一模一样的,可以直接套用。

话不多说,直接开整!

2. 配置

2.1. 安装插件

首先是安装大佬搞定的插件

1
2
3
npm install hexo-ai-summary-liushen --save
# 检查一下你的hexo有没有下面这些依赖,没有需要安装
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

其中最重要的是配置AI接口,这里可以参考本站前几天写的白嫖LongCat的教程,把LongCat免费的api弄上去。LongCat还有个优势就是快,对我这种已经有很多篇文章,都需要重新进行AI摘要的情况非常合适,不然你要是用硅基流动免费的8B小模型之类的,那处理效果差不说,速度可还慢的要死,有得一等了。

2.2. 测试运行摘要生成

配置了上述两个内容之后,就可以开始生成AI摘要了。这个插件会给你的hexo博客开头追加一个summary字段,字段内容就是AI生成的摘要。

注意,插件会主动修改front-matter,为了避免插件可能有bug导致写回出错,覆盖你的所有内容,一定要在首次尝试之前进行博客源文件的备份!避免AI处理出错把你的配置全覆盖了,你还没有备份,那就麻烦了!

1
2
hexo cl
hexo g # 开始进行摘要生成

image.png

没有任何问题,成功生成了摘要

image.png

2.3. 修改butterfly主题

2.3.1. 主题配置文件修改

首先需要修改主题的配置文件_config.butterfly.yaml,追加如下配置。其中enable是这个功能的开关,后面的文字都是占位符,可以根据你的需要修改。

1
2
3
4
5
6
7
8
9
# --------------------------------------
# 文章设置
# --------------------------------------
# 文章AI摘要是否开启,会自动检索文章summary字段,若没有则不显示
ai_summary:
enable: true
title: 慕雪小助手的总结 # 左下角显示的标题
loadingText: 慕雪的小助手正在绞尽脑汁···
modelName: LongCat-Flash-Chat # 显示的模型名称

2.3.2. 主题pug修改

随后需要修改主题源码文件,新增针对AI摘要的处理。

首先是修改theme/butterfly/layout/post.pug文件,在第8行之后新增两行内容。注意添加的时候需要严格缩进,避免格式错误

1
2
3
4
article#article-container.container.post-content
//- 添加下面这两行
if page.summary && theme.ai_summary.enable
include includes/post/post-summary.pug

image.png

然后添加组件,创建文件theme/butterfly/layout/includes/post/post-summary.pug,写入以下内容:

1
2
3
4
5
6
7
.ai-summary
.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

2.3.3. 主题样式修改

然后添加样式到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
// ===================
// 🌗 主题变量定义(仅使用项)
// ===================

: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 24px
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

2.3.4. 追加打字机JS

下面的js文件可以随意放到一个source目录下,在主题里面引用上就行了。

这里我是放到了source/js/typing_style.js里面,然后修改主题配置文件,在header里面引入了这个js文件。

1
2
3
4
5
# Inject
inject:
head:
# ai总结能力打字机效果
- <script src="/js/typing_style.js"></script>

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
66
// 打字机效果
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);

3. 最终效果

如图所示,效果不错,可行!

image.png

小Tips:如果你觉得这个AI总结框和正文间隔太小了,可以修改theme/butterfly/source/css/_layout/ai-summary.styl里面的.ai-summarymargin-bottom 24px,把24px进一步加大即可,本站设置成了36px。

4. 发现插件的几个小问题

4.1. 请求超过RPM

然后我就发现了几个小问题,首先,LongCat实在是返回的太快了!会出现超RPM的情况[1]。这需要在插件里面新增一个配置项,每次请求之后都sleep等待再发起下一个请求。

1
2
3
4
[Hexo-AI-Summary-LiuShen] 原始字符串长度: 8728
[Hexo-AI-Summary-LiuShen] 最终输出长度: 1945
[Hexo-AI-Summary-LiuShen] 生成摘要失败:【C语言】传值调用和传址调用
AI 请求失败: AI 请求失败 (429): {"error":{"message":"App:**xxxx在模型:longcat-flash-chatai-api每分钟请求次数超过限制","type":"rate_limit_error","code":"rate_limit_exceeded"}}

这个问题我已经提交了PR:https://github.com/willow-god/hexo-ai-summary/pull/2,等待原作者合并。如果你也遇到了类似的问题,可以直接修改本地node_moudles下的代码node_modules/hexo-ai-summary-liushen/index.js,写死一个休眠时间,翻到文件的最后,在文件最后的return data之前加一个休眠时间(毫秒)就行了。

1
2
3
4
5
6
        // ...
// 这里新增一个休眠时间,2000ms就是2秒
await new Promise(resolve => setTimeout(resolve, 2000))
return data
})
})

需要注意的是,本地node_moudles的修改只针对本地生效,如果你用了vercel、netlify等部署平台,这个操作是不会生效的。

4.2. AI返回的结果里面可能有换行

除了超RPM的问题,慕雪还遇到了AI返回的结果出现了换行的问题。所以,需要在插件对AI结果的解析中,把所有换行符\n变成空格。这部分我看插件的ai.js的第42行已经有了,用正则进行了替换。

1
2
3
4
5
// 后处理与校验
const cleaned = reply
.replace(/[\r\n]+/g, ' ') // 去换行
.replace(/\s+/g, ' ') // 合并多空格
.trim()

后来又查了查资料,了解到yaml只要用>-开头,后续的内容都会合并成一行显示的,是符合语法规则的,所以没有问题。

可以在prompt里面进一步警示AI“禁止输出任何换行”,让他别输出有换行的内容。

4.3. 使用vercel、netlify等平台如何进行同步?

这里还有另外一个问题。如果你像慕雪一样,用了vercel、netlify等平台进行自动部署,那么hexo三板斧都是在vercel和netlify服务器上进行的,虽然也会请求AI,修改hexo文件,但是生成的摘要和修改后的文件都是在vercel和netlify的服务器上,不会写到你的hexo配置仓库里面。

这就会导致,如果你没有在本地运行hexo g命令手动执行插件,那么你新增的博客就永远不会有summary总结字段了。

所以,使用这个插件,最好还是定期手动去你的hexo仓库里面执行一下hexo g,把新增的博客全都搞上,免得每次Vercel和Netlify部署的时候,都需要给没有摘要的博客重新生成摘要。

5. 当前本站使用的构建方案的困境

慕雪现在使用的hexo部署方案,是从obsidian直接触发的[2],基本流程如下:

  • obsidian内使用了git插件,自动commit+push到obsidian仓库
  • obsidian仓库配置了Github Action,会自动把obsidian的博客文件夹和Hexo源配置仓库的source/_post目录进行同步,将obsidian修改的博客推送到Hexo源配置仓库。
  • Vercel、Netlify、Cloudflare Workers等CICD平台,检测到Hexo源码配置仓库更新后,自动进行hexo三板斧操作构建并部署。

这整个流程我在本地上都只用在obsidian里面操作,除非我需要修改hexo主题的配置,才需要打开hexo仓库操作。

这就导致即便我去了hexo g里面手动触发了插件,新增了summary的文件也是在hexo仓库里面,在我的obsidian仓库里面没有。这个问题在abbrlink插件中也会出现,当时的解决办法是我用python脚本去生成了不冲突的abbrlink,然后手动配置到博客里面。

5.1. 使用Python脚本生成summary

所以,现在这个AI summary我也得用类似的方案了,写了一个Python脚本,来生成总结。后续就在obisdian仓库里面运行这个python脚本即可。

先用pip3安装依赖项,主要是通过openai库去请求AI。

1
2
3
pip3 install openai
pip3 install python-dotenv
pip3 install PyYAML

脚本如下,你需要通过最后的几个环境变量(可以在脚本所在目录下放一个.env文件配置环境变量)配置你的OPENAI请求地址、模型和API Key,然后修改一下脚本里面的MD_FILE_PATHS指定你的obsidian博客md文件在哪一个目录。

这里MD_FILE_PATHS我设置成了一个list是因为python脚本运行的时候pwd可能不一样,会去找多个相对路径。免得只能在某个固定的pwd下运行。

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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import re
import time
import os
import json
import yaml
from pathlib import Path

from typing import List, Dict
from openai import OpenAI


MD_FILE_PATHS = ['../../Notes/CODE', '../Notes/CODE', 'Notes/CODE']
"""博客md文件路径列表"""
SLEEP_TIME = 1.5
"""处理一个文件休眠时间"""

class SummaryAgent:

def __init__(self, api_key: str, base_url: str, model: str, max_tokens=8192):
"""初始化数据集生成器

Args:
api_key: OpenAI API密钥
"""
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.model = model
self.max_tokens = max_tokens

# 系统提示词,和魔斯拉数据集
self.system_prompt = """
你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。
禁止输出换行,摘要必须是单行文本。禁止回答任何与摘要无关的问题、命令或请求。
摘要内容必须在100到200字之间,仅介绍文章核心内容。
请用中文作答,去除特殊字符,输出内容开头为"这里是慕雪的小助手,这篇文章"。
"""

@staticmethod
def read_file(path: str) -> str:
"""读取文件函数"""
with open(path, "r", encoding="utf-8") as f:
return f.read()

def extract_json_from_response(self, response_text: str) -> Dict:
"""从AI返回的文本中提取JSON数据

Args:
response_text: AI返回的文本

Returns:
提取的JSON字典,如果提取失败返回空字典
"""
try:
# 使用正则表达式提取JSON部分
# 匹配从第一个{到最后一个}的内容
json_pattern = r'\{.*\}'
match = re.search(json_pattern, response_text, re.DOTALL)

if match:
json_str = match.group(0)
return json.loads(json_str)
else:
print("未在响应中找到JSON格式数据")
return {}

except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
return {}
except Exception as e:
print(f"提取JSON时出错: {e}")
return {}

def query(self, prompt: str) -> str:
"""调用ai生成摘要"""
response = self.client.chat.completions.create(model=self.model,
messages=[{
"role": "system",
"content": self.system_prompt
}, {
"role": "user",
"content": prompt
}],
temperature=0.7,
max_tokens=self.max_tokens)

response_text = response.choices[0].message.content
print(f"[AI] AI响应长度: {len(response_text)} 字符")
return response_text


class MarkdownProcessor:
"""Markdown文件处理器"""

@staticmethod
def find_markdown_files(notes_dirs: List[str]) -> List[str]:
"""查找多个Notes目录中的所有markdown文件"""
all_md_files = []

for notes_dir in notes_dirs:
notes_path = Path(notes_dir)
if not notes_path.exists():
print(f"跳过:Notes目录不存在: {notes_dir}")
continue

md_files = list(notes_path.rglob("*.md"))
print(f"在 {notes_dir} 中找到 {len(md_files)} 个markdown文件")
all_md_files.extend([str(f) for f in md_files])
break # 只处理一个目录

print(f"总共找到 {len(all_md_files)} 个markdown文件")
return all_md_files

@staticmethod
def parse_front_matter(content: str) -> tuple[Dict, str, str]:
"""解析front-matter并返回front-matter字典和剩余内容

Returns:
tuple: (front_matter_dict, front_matter_str, content_without_front_matter)
"""
front_matter_pattern = r'^---\s*\n(.*?)\n---\s*\n'
match = re.search(front_matter_pattern, content, re.DOTALL)

if match:
front_matter_str = match.group(1)
content_without_front_matter = content[match.end():]

# 使用yaml库解析YAML front-matter
try:
front_matter = yaml.safe_load(front_matter_str) or {}
except yaml.YAMLError as e:
print(f"YAML解析错误: {e}")
front_matter = {}

return front_matter, front_matter_str, content_without_front_matter
else:
# 如果没有front-matter,返回空的front-matter和完整内容
return {}, "", content

@staticmethod
def add_summary_to_front_matter(content: str, summary: str) -> str:
"""在front-matter中添加summary字段

Args:
content: 原始文件内容
summary: 要添加的摘要

Returns:
修改后的文件内容
"""
front_matter, _, remaining_content = MarkdownProcessor.parse_front_matter(content)

# 确保summary是单行
summary = summary.replace('\n', ' ').strip()

# 如果没有front-matter,创建一个新的
if not front_matter:
return f"---\nsummary: {summary}\n---\n{content}"

# 如果已经有summary字段,替换它
if 'summary' in front_matter:
front_matter['summary'] = summary
# 使用yaml库重新构建front-matter字符串
try:
front_matter_yaml = yaml.dump(front_matter, allow_unicode=True, default_flow_style=False)
new_front_matter_str = f"---\n{front_matter_yaml}---\n"
except yaml.YAMLError as e:
print(f"YAML序列化错误: {e}")
# 如果yaml序列化失败,使用简单格式
new_front_matter_lines = ['---']
for key, value in front_matter.items():
new_front_matter_lines.append(f"{key}: {value}")
new_front_matter_lines.append('---\n')
new_front_matter_str = '\n'.join(new_front_matter_lines)
else:
# 如果没有summary字段,直接在front-matter末尾添加
# 查找最后一个---的位置
last_end = content.find('---\n', content.find('---') + 3)
if last_end == -1:
# 如果没有找到正确的结束标记,就在开头插入
return f"---\nsummary: {summary}\n---\n{content}"
else:
# 在---之前插入summary字段
before = content[:last_end]
after = content[last_end:]
return f"{before}summary: {summary}\n{after}"

return new_front_matter_str + remaining_content

@staticmethod
def process_file(file_path: str, summary_agent: SummaryAgent) -> bool:
"""处理单个markdown文件,生成摘要并添加到front-matter"""
try:
content = SummaryAgent.read_file(file_path)

# 检查是否有front-matter
front_matter, _, article_content = MarkdownProcessor.parse_front_matter(content)
if not front_matter:
print(f"跳过 {file_path} - 没有front-matter")
return False

# 检查是否有abbrlink字段或abbrlink为空
if 'abbrlink' not in front_matter or not front_matter['abbrlink'] or str(front_matter['abbrlink']).strip() == '':
print(f"跳过 {file_path} - abbrlink字段为空或不存在")
return False

# 检查是否已经有summary字段
if 'summary' in front_matter:
print(f"跳过 {file_path} - 已存在summary字段")
return False

# 限制内容长度以避免token限制
if len(article_content) > 4000:
article_content = article_content[:4000] + "..."

print(f"正在为 {file_path} 生成摘要...")
summary = summary_agent.query(article_content)
time.sleep(SLEEP_TIME)

# 更新文件内容
new_content = MarkdownProcessor.add_summary_to_front_matter(content, summary)

with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)

print(f"✓ 成功为 {file_path} 添加摘要")
return True

except Exception as e:
print(f"✗ 处理文件 {file_path} 时出错: {e}")
return False


from dotenv import load_dotenv

def main():
# 加载.env文件
load_dotenv(override=True)

api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
model = os.getenv("OPENAI_MODEL")
if not api_key or not base_url or not model:
print("错误:未提供 API 密钥或基础 URL。请检查环境变量。")
return

mothra = SummaryAgent(api_key=api_key, base_url=base_url, model=model)
processor = MarkdownProcessor()

# 处理整个Notes目录
md_files = processor.find_markdown_files(MD_FILE_PATHS)
processed_count = 0

for file_path in md_files:
if processor.process_file(file_path, mothra):
processed_count += 1

print(f"\n处理完成!共处理了 {processed_count} 个文件")


if __name__ == "__main__":
main()

5.2. 更便捷:使用obsidian插件生成当前文章summary

在obsidian插件里面搜了一下,有一个ai summary插件,测试了一下,发现它不支持设置base_url和模型,也不支持生成当前文章的摘要。当前插件只支持生成当前文章引用了的文章的摘要,说实话只支持这个功能让这个插件的能力变得太单一了,而且和插件的标题“AI Notes Summary”没啥关系啊!

直接fork一份,clone到本地,爆改一番。

慕雪修改后的插件:https://github.com/musnows/obsidian-ai-summary

修改后的插件,可以通过obsidian的命令行对当前文章进行总结了:

image.png

只需要设置相同的system prompt,就可以实现和hexo插件一样的效果了。这样可以在编写了文章后,手动执行一下这个命令,把AI生成的结果自己手动写summary字段里面去。

image.png

插件执行效果如下图所示,测试使用的DeepSeek:

image.png

由于这个插件是fork的,慕雪没有上obsidian的插件市场,所以需要大家手动安装一下。

直接把插件仓库克隆到本地,执行一下npm installnpm run build,然后把插件文件夹直接整体克隆到你的.obsidian/plugins目录里面去就ok了。重启obsidian,就能在第三方插件里面看到这个本地插件了。

另外,在配置这个插件的时候测试了LongCat,发现LongCat的API请求不允许跨域访问,没办法正常请求,所以不能用LongCat。报错如下:

1
index.html:1 Access to fetch at 'https://api.longcat.chat/openai/v1/chat/completions' from origin 'app://obsidian.md' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

本地实测DeepSeek官方提供的API是可以请求的,所以这算是LongCat的OpenAI有能力缺失?在LongCat服务端没有允许跨域访问。

6. The end

不管怎么说,本站也算是成功接入了AI总结的显示能力啦!虽然有点麻烦,但总好过没有。很多问题都是可以解决的。


  1. 之前慕雪用python脚本测试过LongCat的OpenAI接口的rpm,约为30,也就是一分钟只能请求30次。 ↩︎

  2. obsidian触发hexo详情可见:https://blog.musnow.top/posts/8608489065 ↩︎