前言
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 switchBtn: false btnLink: https://afdian.net/item/886a79d4db6711eda42a52540025c377 randomNum: 3 basicWordCount: 200 key: xxxx Referer: https://xx.xx/ customApi: enable: true url: http://your-api-server.com/v1/chat/completions method: POST headers: Authorization: Bearer YOUR_API_KEY Content-Type: application/json openAIFormat: true model: gpt-3.5-turbo systemPrompt: 你是一个专业的文章摘要助手,请为以下文章生成一段简洁的摘要。 requestBody: content: content url: url wordCount: wordCount queryParams: content: content url: url responsePath: choices[0].message.content
|
2.3 配置参数详解
必需参数
enable: true:启用自定义 API
url:您的 AI API 完整地址
method:HTTP 请求方法,通常是 POST
OpenAI 格式参数(推荐)
openAIFormat: true:启用 OpenAI 兼容格式
model:模型名称,如 gpt-3.5-turbo、gpt-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) { if (customApi && customApi.enable && customApi.url) { await aiAbstractCustom(num); return; } 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 ""; 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 = {}; 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 { 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); } }
|
四、配置示例
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 配置检查
- 确认
mode: local
- 确认
customApi.enable: true
- 确认 API URL 正确
- 确认 API Key 已替换为真实密钥
6.2 重新生成
1 2
| hexo clean hexo generate
|
6.3 启动服务器
6.4 调试检查
- 打开浏览器开发者工具(F12)
- 切换到 Network 标签
- 刷新页面,查看是否有对 API 的请求
- 检查请求头和请求体是否正确
- 查看响应数据格式
- 检查 Console 标签中的日志信息
6.5 预期结果
- 文章页面应显示 AI 摘要框
- 点击刷新按钮应调用自定义 API
- 控制台应显示请求日志
- 摘要应正常显示
七、优化建议
7.1 添加错误重试机制
可以在代码中添加失败重试逻辑,提高稳定性。
7.2 添加缓存机制
对于同一篇文章,可以缓存摘要结果,避免重复请求。
7.3 添加加载状态
可以在请求过程中显示更友好的加载动画。
7.4 支持流式响应
如果 API 支持流式响应,可以实现逐字显示效果。
八、总结
通过以上步骤,您已经成功为 AnZhiYu 主题添加了自定义 AI 摘要平台的支持。主要步骤包括:
- ✅ 在配置文件中添加
customApi 配置
- ✅ 修改 JavaScript 代码支持自定义 API 调用
- ✅ 添加路径解析功能,支持复杂的响应结构
- ✅ 实现 OpenAI 兼容格式和自定义格式的支持
现在您可以:
- 使用自己的 AI API
- 完全控制摘要生成过程
- 保护数据隐私
- 自定义提示词和参数
希望本文对您有所帮助!如有问题,欢迎留言讨论。
参考链接