前言
使用AI能够快速从大量文本中提取关键信息,为读者提供清晰、简洁的文章概览。
我检索了网上的一些开源的文章生成器,但是他们基本上需要付费使用,并且是直接使用前端生成摘要的,不太安全,可能会造成token的盗用。我在想是否有一种安全的方法实现这个功能。
使用SSG来安全实现
SSG通常指的是"静态网站生成器"(Static Site Generator),这是一种软件,用于将文本内容(通常是Markdown或类似的标记语言)转换成静态HTML页面。这些页面可以部署在任何服务器上,无需复杂的服务器端逻辑即可运行。
我博客使用的是 valaxy,感谢云佬造的轮子。博客很漂亮,自由度高!
编写一个valaxy插件,在Build时就对相关的文章进行生成摘要。你是不是在担心,每次构建都会去重复消耗token? 别急,我后面说
// 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.default
和 D1
作为缓存,只有同一篇文章第一次才会消耗。
配置几个环境变量
名称 | 说明 |
---|---|
API | GPT API或者其他镜像站的地址 |
AUTH | Bearer sk-******* 看这个应该清楚吧 |
MODEL | 使用的模型 |
TOKEN | 上面请求那里需要填的,两边一样就好 |
::: tip
什么你问我AUTH在哪找捏?论坛里面经常有大佬发免费的
:::
新建D1数据库,在Workers中进行绑定
D1表结构:
其他细节我就不多说了,欢迎交流!