本教程适用于 halo 博客给文章添加 ai 摘要,其他博客也类似,基本适用。
本教程在 halo2.19 版本+Dream 主题上测试通过,其他版本和主题不保证完全适用

前置准备

  1. cloudflare账号,需要使用 workers 和 D1 数据库
  2. 兼容openai协议的 apikey
  3. 托管于cloudflare的自定义域名(可选)

配置workers

在 cloudflare上新建 workers,如图所示:
image.png

新建以后,点击编辑代码,将如下代码粘贴进去

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 的环境变量
image.png

  • 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 消耗。配置如下:

新建一个数据库,名称随意
image.png

数据库中建一个名为posts的表,必须是该名字
image.png

表包含两个字段,hashsymmary,类型均为 text
image.png
最后,将数据库绑定到 workers 上
image.png

此外,如果有自定义域名,也可以为该 worker 设置自定义域名,因为默认的 cloudflare 域名可能被墙了。

创建博客js文件

下载下面的 js 代码并编辑
https://oss.mahaonan.fun/2024/09/18/1726644885_oxEvmdgM.js
修改第 381 行的链接和 token 为你自己 workder 地址和变量配置的 token
image.png
然后将修改后的文件传到任意一个服务器上的位置,或者是任意博客可以访问的位置,我这里是放到了对象存储上。

halo代码注入

在 halo 管理后台 设置下的代码注入模块
image.png

在页脚部分放入如下代码:

<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>

上述代码注入主要做了以下几件事情:

  1. 引入前面摘要相关 js
  2. 处理了页面路径,保证摘要只在文章页面生效
  3. 兼容页面加载完成事件和pjax:end 事件,保证 pjax 开启后也能生效

参考资料

https://blog.csun.site/posts/0.html