为Hexo AnZhiYu主题配置自定义AI摘要平台

前言

AnZhiYu 主题默认支持两种 AI 摘要模式:

  • Tianli GPT:使用第三方 Tianli 服务
  • Local 模式:使用文章 Front Matter 中预设的 ai 字段

如果您想使用自己的 AI API(如自建的 OpenAI 兼容服务、Claude API 等),就需要进行自定义配置。本文将详细介绍如何修改主题代码和配置文件,实现自定义 AI 平台的支持。

一、准备工作

1.1 所需环境

  • Hexo 博客已搭建完成
  • 使用 AnZhiYu 主题
  • 拥有一个可用的 AI API(支持 OpenAI 兼容格式或自定义格式)

1.2 了解 API 格式

在开始之前,您需要了解您的 AI API 的:

  • API 地址(URL)
  • 请求方法(GET/POST)
  • 请求头格式(如 Authorization)
  • 请求体格式
  • 响应数据格式

本文将以 OpenAI 兼容格式为例进行讲解。

二、修改配置文件

2.1 打开配置文件

编辑 _config.anzhiyu.yml 文件,找到 post_head_ai_description 配置项(通常在文件末尾部分)。

2.2 添加自定义 API 配置

post_head_ai_description 下添加 customApi 配置:

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
post_head_ai_description:
enable: true
gptName: Chat
mode: local # 必须设置为 local 才能使用自定义 API
switchBtn: false
btnLink: https://afdian.net/item/886a79d4db6711eda42a52540025c377
randomNum: 3
basicWordCount: 200 # 从文章中截取的字符数,用于生成摘要
key: xxxx # 当 mode 为 tianli 时使用
Referer: https://xx.xx/ # 当 mode 为 tianli 时使用

# 自定义 AI 平台配置(当 mode 为 local 时使用)
customApi:
enable: true # 是否启用自定义 AI API
url: http://your-api-server.com/v1/chat/completions # 您的 AI API 地址
method: POST # 请求方法:POST 或 GET
headers: # 自定义请求头
Authorization: Bearer YOUR_API_KEY # API 密钥(请替换为真实密钥)
Content-Type: application/json

# OpenAI 兼容格式配置
openAIFormat: true # 是否使用 OpenAI 格式
model: gpt-3.5-turbo # 模型名称(OpenAI 格式时使用)
systemPrompt: 你是一个专业的文章摘要助手,请为以下文章生成一段简洁的摘要。 # 系统提示词(可选)

# 自定义请求体格式(当 openAIFormat 为 false 时使用)
requestBody:
content: content # 文章内容字段名
url: url # 文章URL字段名(可选)
wordCount: wordCount # 期望字数(可选)

# URL 参数映射(GET 方法时需要)
queryParams:
content: content
url: url

# 响应数据路径(支持数组索引,如 choices[0].message.content)
responsePath: choices[0].message.content

2.3 配置参数详解

必需参数

  • enable: true:启用自定义 API
  • url:您的 AI API 完整地址
  • method:HTTP 请求方法,通常是 POST

OpenAI 格式参数(推荐)

  • openAIFormat: true:启用 OpenAI 兼容格式
  • model:模型名称,如 gpt-3.5-turbogpt-4
  • systemPrompt:系统提示词,用于指导 AI 的行为

认证参数

  • headers.Authorization:API 密钥,格式通常是 Bearer YOUR_API_KEY

响应解析参数

  • responsePath:响应中摘要数据的路径
    • 支持嵌套路径:data.summary
    • 支持数组索引:choices[0].message.content

三、修改 JavaScript 代码

3.1 定位文件

需要修改的文件位于:

1
themes/anzhiyu/source/js/anzhiyu/ai_abstract.js

3.2 修改配置读取

在文件开头,找到配置解构部分,添加 customApi

1
2
3
4
5
6
7
8
9
10
11
const {
randomNum,
basicWordCount,
btnLink,
key: AIKey,
Referer: AIReferer,
gptName,
switchBtn,
mode: initialMode,
customApi, // 添加这一行
} = GLOBAL_CONFIG.postHeadAiDescription;

3.3 修改 aiAbstractLocal 函数

找到 aiAbstractLocal 函数,修改为异步函数并添加自定义 API 调用逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function aiAbstractLocal(num = basicWordCount) {
// 如果启用了自定义 API,则调用自定义 API
if (customApi && customApi.enable && customApi.url) {
await aiAbstractCustom(num);
return;
}

// 否则使用文章 Front Matter 中的 ai 字段
const strArr = postAI.split(",").map(item => item.trim());
if (strArr.length !== 1) {
let randomIndex = Math.floor(Math.random() * strArr.length);
while (randomIndex === lastAiRandomIndex) {
randomIndex = Math.floor(Math.random() * strArr.length);
}
lastAiRandomIndex = randomIndex;
startAI(strArr[randomIndex]);
} else {
startAI(strArr[0]);
}
setTimeout(() => {
aiTitleRefreshIcon.style.opacity = "1";
}, 600);
}

3.4 添加路径解析辅助函数

aiAbstractLocal 函数之前添加辅助函数,用于解析嵌套路径和数组索引:

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
// 辅助函数:从嵌套对象中获取值,支持数组索引
function getNestedValue(obj, path) {
if (!path || !obj) return "";

// 处理路径,支持数组索引,如 "choices[0].message.content"
const parts = path.match(/[^.\[\]]+|\[\d+\]/g) || [];
let current = obj;

for (const part of parts) {
if (part.startsWith("[") && part.endsWith("]")) {
// 数组索引
const index = parseInt(part.slice(1, -1));
if (Array.isArray(current) && current[index] !== undefined) {
current = current[index];
} else {
return "";
}
} else {
// 对象属性
if (current && typeof current === "object" && part in current) {
current = current[part];
} else {
return "";
}
}
}

return typeof current === "string" ? current : "";
}

3.5 创建 aiAbstractCustom 函数

添加新的函数来处理自定义 API 调用:

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
async function aiAbstractCustom(num) {
indexI = 0;
indexJ = 1;
clearTimeouts();
animationRunning = false;
elapsed = 0;
observer.disconnect();

num = Math.max(10, Math.min(2000, num));
const truncateDescription = (title + pageFillDescription).trim().substring(0, num);

try {
let animationInterval = null;
if (animationInterval) clearInterval(animationInterval);
animationInterval = setInterval(() => {
const animationText = "生成中" + ".".repeat(indexJ);
explanation.innerHTML = animationText;
indexJ = (indexJ % 3) + 1;
}, 500);

const method = (customApi.method || "POST").toUpperCase();
const headers = customApi.headers || { "Content-Type": "application/json" };

let requestOptions = {
method: method,
headers: headers,
};

if (method === "POST") {
let requestBody = {};

// OpenAI 格式
const useOpenAIFormat = customApi.openAIFormat === true || customApi.openAIFormat === "true";

if (useOpenAIFormat) {
requestBody = {
model: customApi.model || "gpt-3.5-turbo",
messages: []
};

// 添加系统提示词
if (customApi.systemPrompt) {
requestBody.messages.push({
role: "system",
content: customApi.systemPrompt
});
}

// 构建用户消息
const userPrompt = `请为以下文章生成一段简洁的摘要(约200字):\n\n标题:${title}\n\n内容:${truncateDescription}`;
requestBody.messages.push({
role: "user",
content: userPrompt
});

// 可选参数
if (customApi.temperature !== undefined) {
requestBody.temperature = customApi.temperature;
}
if (customApi.maxTokens !== undefined) {
requestBody.max_tokens = customApi.maxTokens;
}
} else if (customApi.requestBody) {
// 自定义请求体格式
if (customApi.requestBody.content) {
requestBody[customApi.requestBody.content] = truncateDescription;
}
if (customApi.requestBody.url) {
requestBody[customApi.requestBody.url] = location.href;
}
if (customApi.requestBody.wordCount) {
requestBody[customApi.requestBody.wordCount] = num;
}
} else {
// 默认格式
requestBody.content = truncateDescription;
requestBody.url = location.href;
requestBody.wordCount = num;
}

requestOptions.body = JSON.stringify(requestBody);
} else {
// GET 请求:构建 URL 参数
const params = new URLSearchParams();
if (customApi.queryParams) {
if (customApi.queryParams.content) {
params.append(customApi.queryParams.content, truncateDescription);
}
if (customApi.queryParams.url) {
params.append(customApi.queryParams.url, location.href);
}
} else {
params.append("content", truncateDescription);
params.append("url", location.href);
}
const separator = customApi.url.includes("?") ? "&" : "?";
customApi.url = customApi.url + separator + params.toString();
}

// 发送请求
const response = await fetch(customApi.url, requestOptions);
let result;

if (!response.ok) {
result = {
error: `请求失败: ${response.status} ${response.statusText}`,
};
} else {
result = await response.json();
}

clearInterval(animationInterval);

// 解析响应数据
let summaryText = "";
if (result.error) {
summaryText = `摘要获取失败: ${result.error}`;
} else if (customApi.responsePath) {
summaryText = getNestedValue(result, customApi.responsePath);
if (!summaryText) {
summaryText = `摘要获取失败: 响应中未找到路径 ${customApi.responsePath}`;
}
} else {
summaryText = result.summary || result.text || result.content || "";
if (!summaryText) {
summaryText = "摘要获取失败: 响应格式不正确";
}
}

if (result.error && result.error.message) {
summaryText = `摘要获取失败: ${result.error.message}`;
}

summary = summaryText.trim();

setTimeout(() => {
aiTitleRefreshIcon.style.opacity = "1";
}, 300);

if (summary) {
startAI(summary);
} else {
startAI("摘要获取失败,请检查 API 配置和响应格式");
}
} catch (error) {
console.error("自定义 AI API 调用失败:", error);
explanation.innerHTML = "发生异常: " + error.message;
startAI("摘要获取失败: " + error.message);
}
}

3.6 修改 aiAbstract 函数

确保 aiAbstract 函数支持异步调用:

1
2
3
4
5
6
7
async function aiAbstract(num = basicWordCount) {
if (mode === "tianli") {
await aiAbstractTianli(num);
} else {
await aiAbstractLocal(num); // 改为 await
}
}

四、配置示例

4.1 OpenAI 兼容 API 示例

1
2
3
4
5
6
7
8
9
10
customApi:
enable: true
url: https://api.openai.com/v1/chat/completions
method: POST
headers:
Authorization: Bearer sk-xxxxx
openAIFormat: true
model: gpt-3.5-turbo
systemPrompt: 你是一个专业的文章摘要助手,请为以下文章生成一段简洁的摘要。
responsePath: choices[0].message.content

4.2 自建 API 示例(自定义格式)

1
2
3
4
5
6
7
8
9
10
11
12
customApi:
enable: true
url: https://your-api.com/summarize
method: POST
headers:
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
openAIFormat: false
requestBody:
content: text
url: article_url
responsePath: summary

4.3 GET 请求示例

1
2
3
4
5
6
7
8
9
10
customApi:
enable: true
url: https://your-api.com/summarize
method: GET
headers:
X-API-Key: YOUR_KEY
queryParams:
content: text
url: article_url
responsePath: result.summary

五、常见问题解决

5.1 API Key 认证失败

问题:返回 401 或 403 错误

解决方案

  • 检查 Authorization header 格式是否正确
  • 确保 API Key 有效且有足够权限
  • 如果 API 不需要 Bearer 前缀,直接写密钥即可

5.2 请求格式错误

问题:服务器返回 400 错误,提示请求格式不正确

解决方案

  • 检查 openAIFormat 是否正确设置
  • 查看浏览器控制台中的请求体,确认格式是否符合 API 要求
  • 根据 API 文档调整 requestBody 配置

5.3 响应解析失败

问题:摘要显示“响应中未找到路径“

解决方案

  • 在浏览器控制台查看实际响应结构
  • 根据响应格式调整 responsePath
  • 支持嵌套路径:data.summary
  • 支持数组索引:choices[0].message.content

5.4 字数设置

问题:生成的摘要字数不符合预期

解决方案

  • basicWordCount:控制从文章中截取的输入字符数
  • 提示词中的字数要求:控制生成摘要的长度
  • 可以在配置中添加 maxTokens 参数限制输出长度

六、测试步骤

6.1 配置检查

  1. 确认 mode: local
  2. 确认 customApi.enable: true
  3. 确认 API URL 正确
  4. 确认 API Key 已替换为真实密钥

6.2 重新生成

1
2
hexo clean
hexo generate

6.3 启动服务器

1
hexo server

6.4 调试检查

  1. 打开浏览器开发者工具(F12)
  2. 切换到 Network 标签
  3. 刷新页面,查看是否有对 API 的请求
  4. 检查请求头和请求体是否正确
  5. 查看响应数据格式
  6. 检查 Console 标签中的日志信息

6.5 预期结果

  • 文章页面应显示 AI 摘要框
  • 点击刷新按钮应调用自定义 API
  • 控制台应显示请求日志
  • 摘要应正常显示

七、优化建议

7.1 添加错误重试机制

可以在代码中添加失败重试逻辑,提高稳定性。

7.2 添加缓存机制

对于同一篇文章,可以缓存摘要结果,避免重复请求。

7.3 添加加载状态

可以在请求过程中显示更友好的加载动画。

7.4 支持流式响应

如果 API 支持流式响应,可以实现逐字显示效果。

八、总结

通过以上步骤,您已经成功为 AnZhiYu 主题添加了自定义 AI 摘要平台的支持。主要步骤包括:

  1. ✅ 在配置文件中添加 customApi 配置
  2. ✅ 修改 JavaScript 代码支持自定义 API 调用
  3. ✅ 添加路径解析功能,支持复杂的响应结构
  4. ✅ 实现 OpenAI 兼容格式和自定义格式的支持

现在您可以:

  • 使用自己的 AI API
  • 完全控制摘要生成过程
  • 保护数据隐私
  • 自定义提示词和参数

希望本文对您有所帮助!如有问题,欢迎留言讨论。

参考链接