本教程适用于 halo 博客给文章添加 ai 摘要,其他博客也类似,基本适用。
本教程在 halo2.19 版本+Dream 主题上测试通过,其他版本和主题不保证完全适用
前置准备
- cloudflare账号,需要使用 workers 和 D1 数据库
- 兼容openai协议的 apikey
- 托管于cloudflare的自定义域名(可选)
配置workers
在 cloudflare上新建 workers,如图所示:
新建以后,点击编辑代码,将如下代码粘贴进去
function addHeaders(response) {
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set(
'Access-Control-Allow-Methods',
'GET,HEAD,OPTIONS,POST,PUT',
)
response.headers.set(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization',
)
}
async function sha256(message) {
// encode as UTF-8
const msgBuffer = await new TextEncoder().encode(message);
// hash the message
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
// convert bytes to hex string
return [...new Uint8Array(hashBuffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname.startsWith('/api/summary')) {
let response
if (request.method == 'OPTIONS') {
response = new Response('')
addHeaders(response)
return response
}
if (request.method !== 'POST') {
return new Response('error method', { status: 403 });
}
if (url.searchParams.get('token') !== env.TOKEN) {
return new Response('error token', { status: 403 });
}
let body = await request.json()
const hash = await sha256(body.content)
const cache = caches.default
let cache_summary = await cache.match(`http://objects/${hash}`)
if (cache_summary) {
response = new Response(
JSON.stringify({
summary: (await cache_summary.json()).choices[0].message.content
}),
{ headers: { 'Content-Type': 'application/json' } },
)
addHeaders(response)
return response
}
const cache_db = await env.DB.prepare('Select summary from posts where hash = ?').bind(hash).first("summary")
if (cache_db) {
response = new Response(
JSON.stringify({
summary: cache_db
}),
{ headers: { 'Content-Type': 'application/json' } },
)
addHeaders(response)
ctx.waitUntil(cache.put(hash, new Response(
JSON.stringify({
choices: [
{
message: {
content: cache_db,
}
}
]
}),
{ headers: { 'Content-Type': 'application/json' } },
)))
return response
}
const init = {
body: JSON.stringify({
"model": env.MODEL,
"messages": [
{
"role": "system",
"content": "你是一个摘要生成工具,你需要解释我发送给你的内容,不要换行,不要超过200字,不要包含链接,只需要简单介绍文章的内容,不需要提出建议和缺少的东西,不要提及用户.请用中文回答,这篇文章讲述了什么?"
},
{
"role": "user",
"content": body.content
}
],
"safe_mode": false,
"stream": false
}),
method: "POST",
headers: {
"content-type": "application/json;charset=UTF-8",
"Authorization": env.AUTH
},
};
const response_target = await fetch(env.API, init);
const resp = await response_target.json()
response = new Response(
JSON.stringify({
summary: resp.choices[0].message.content
}),
{ headers: { 'Content-Type': 'application/json' } },
)
ctx.waitUntil(cache.put(`http://objects/${hash}`, response_target))
await env.DB.prepare('INSERT INTO posts (hash, summary) VALUES (?1, ?2)').bind(hash, resp.choices[0].message.content).run()
addHeaders(response)
return response
}
return new Response('Hello World!');
},
};
然后配置 workers 的环境变量
- API:接入的ai 地址,例如
https://api.openai.com/v1/chat/completions
- AUTH: 授权 apiKey, 例如
Bearer sk-xxx
- MODEL:模型,例如
gpt-4o-mini
- TOKEN:自定义的授权密钥,例如 `secret-blog-ai
其实 token 还是会暴露在链接上,但是无所谓,因为暴露出去的 ai 也只能做总结,监控好即可。可以选择一些便宜的国产模型,例如 doubao,deepseek 等
配置D1数据库
D1 数据库kv 缓存数据库,可以在文本内容一样时,直接返回缓存的摘要,提高访问效率以及节省 ai 消耗。配置如下:
新建一个数据库,名称随意
数据库中建一个名为posts
的表,必须是该名字
表包含两个字段,hash
和symmary
,类型均为 text
最后,将数据库绑定到 workers 上
此外,如果有自定义域名,也可以为该 worker 设置自定义域名,因为默认的 cloudflare 域名可能被墙了。
创建博客js文件
下载下面的 js 代码并编辑
https://oss.mahaonan.fun/2024/09/18/1726644885_oxEvmdgM.js
修改第 381 行的链接和 token 为你自己 workder 地址和变量配置的 token
然后将修改后的文件传到任意一个服务器上的位置,或者是任意博客可以访问的位置,我这里是放到了对象存储上。
halo代码注入
在 halo 管理后台 设置下的代码注入模块
在页脚部分放入如下代码:
<script src="前面 js 的地址"></script>
<script data-pjax defer>
function matchPath(pattern, path) {
// 将模式转换为正则表达式
const regexPattern = pattern
.replace(/\//g, "\\/") // 转义斜杠
.replace(/\*\*/g, ".*") // ** 匹配任意字符(包括斜杠)
.replace(/\*/g, "[^\\/]*"); // * 匹配除斜杠外的任意字符
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
function initializeScript() {
const include_path = ["/archives/**"];
// 检查当前 URL 是否在 exclude_path 中
if (!include_path.some(pattern => matchPath(pattern, window.location.pathname))) {
return;
}
new ChucklePostAI({
// 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面
// el: 'body > section > div > div > div > div:nth-child(2)',
el: '.card-content.main',
summary_directly: true,
rec_method: 'web',
// 若网站开启了 PJAX, 则开启
pjax: true,
})
}
// 监听页面加载完成事件
$(document).ready(function () {
initializeScript();
});
// 监听 pjax:end 事件
$(document).on('pjax:end', function () {
initializeScript();
});
</script>
上述代码注入主要做了以下几件事情:
- 引入前面摘要相关 js
- 处理了页面路径,保证摘要只在文章页面生效
- 兼容页面加载完成事件和pjax:end 事件,保证 pjax 开启后也能生效