博客AI摘要生成

前言

使用AI能够快速从大量文本中提取关键信息,为读者提供清晰、简洁的文章概览。
我检索了网上的一些开源的文章生成器,但是他们基本上需要付费使用,并且是直接使用前端生成摘要的,不太安全,可能会造成token的盗用。我在想是否有一种安全的方法实现这个功能。

使用SSG来安全实现

SSG通常指的是"静态网站生成器"(Static Site Generator),这是一种软件,用于将文本内容(通常是Markdown或类似的标记语言)转换成静态HTML页面。这些页面可以部署在任何服务器上,无需复杂的服务器端逻辑即可运行。

我博客使用的是 valaxy,感谢云佬造的轮子。博客很漂亮,自由度高!

编写一个valaxy插件,在Build时就对相关的文章进行生成摘要。你是不是在担心,每次构建都会去重复消耗token? 别急,我后面说:smile:

// node/index.tx
import { defineValaxyAddon } from "valaxy";
import pkg from "../package.json";
import consola from "consola";
import fs from "fs-extra";
import matter from "gray-matter";
import axios from 'axios'
export const addonAiSummary = defineValaxyAddon((options) => ({
  name: pkg.name,
  enable: true,
  ...options,
  setup(valaxy) {
    valaxy.hook("vue-router:extendRoute", async (route) => {
      if (route.meta.frontmatter.excerpt_type === "ai") {
        try {
          let path = route.components.get("default") as string;
          const md = fs.readFileSync(path, "utf-8");
          const { content } = matter(md);
          let resp = await axios.post(
            "https://*****/api/summary?token=*****", // 记得修改哦!
            {
              content: content.replaceAll(/<[^>]+>/gm, "").replaceAll(/<!-- valaxy-encrypt-start:(?<password>\w+) -->(?<content>.*?)<!-- valaxy-encrypt-end -->/gs,""),
            },
            {
              headers: {
                "Content-Type": "application/json",
              },
            },
          );
          route.meta.frontmatter.ai_excerpt = resp.data.summary;
          route.addToMeta(
            {
              excerpt: resp.data.summary,
            }
          )
          consola.info(`[${pkg.name}]:`,route.meta.frontmatter)
        } catch (e) {
          consola.error(`[${pkg.name}]:`,e);
        }
      }
    });
  },
}));

搭建AI摘要生成的后端

这里我们使用 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
				}),
				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!');
	},
};

这里我回答一下上面重复生成摘要的问题:
这里我使用了 Cloudflare caches.defaultD1 作为缓存,只有同一篇文章第一次才会消耗。

配置几个环境变量

名称 说明
API GPT API或者其他镜像站的地址
AUTH Bearer sk-******* 看这个应该清楚吧
MODEL 使用的模型
TOKEN 上面请求那里需要填的,两边一样就好

::: tip
什么你问我AUTH在哪找捏?论坛里面经常有大佬发免费的
:::

新建D1数据库,在Workers中进行绑定

D1表结构:

其他细节我就不多说了,欢迎交流!

10 Likes

typecho也想要,求教程 :yum:

1 Like

typecho没有用过哎 :sweat_smile: ,一般静态博客可以使用这个方案


之前看到的

厉害

厉害哇

2 Likes

有用到vuepress 博客中的吗

可以看下这个插件
https://github.com/idealclover/AISummary-Typecho/

2 Likes

也可以看看这个插件

1 Like

原来是冰剑大佬!
这个插件后续还会添加openai格式的接口吗?

有没有免费可用的wordpress插件版?

不是大佬。。

不会加了,主打一个自己够用。

我也想一个:heavy_plus_sign:1

1 Like

这个插件用了一下,但是后台生成摘要时会时不时无响应然后net error另外与superlink插件有冲突导致superlink插件不工作,不知道是不是我全站pjax的原因

可能跟文章长度有关,默认是gpt-3.5-turbo-16k模型。我测试批量提交生成容易出现超时,所以现在需要生成的就每篇手动点一下

谢谢分享,刚需要这.

From #dev to 开发调优