mirror of https://github.com/Mabbs/mabbs.github.io
Browse Source
- /_posts/2026-02-08-xslt.md - /_data/other_repo_list.csv - /_tools/ai-summary.js - /_tools/envs_post-receivemaster AR-Backup-2026.02.08
4 changed files with 399 additions and 406 deletions
|
@ -0,0 +1,34 @@ |
|||
--- |
|||
layout: post |
|||
title: 在Google杀死XSLT之后的XML美化方案 |
|||
tags: [XML, Feed, XSLT, 美化] |
|||
--- |
|||
|
|||
即使没有了XSLT,也不能让读者看到光秃秃的XML!<!--more--> |
|||
|
|||
# 起因 |
|||
在半年前,我写了一篇[用XSLT美化博客XML文件](/2025/07/01/xslt.html)的文章,自从那以后,每次我在浏览其他人博客的时候,都会看一眼对方博客有没有给自己的订阅文件做美化。不过就在前段时间,我在浏览某个博客的时候,发现他博客的订阅文件,甚至连最基本的XML文档树都没有显示出来。这时候我打开开发者工具看了一眼源代码,发现他也并没有使用`xml-stylesheet`之类的指令……而且控制台貌似报了些错,好像是出现了什么CSP错误……于是我就想,浏览器显示XML文档树的本质,会不会其实也是一种XSLT?之所以报错也有可能是浏览器在自动引用内置的XSLT时违反了CSP。所以我就问了问谷歌AI,结果似乎真的是这样,比如火狐浏览器就内置了一份[XSLT文件](https://github.com/mozilla-firefox/firefox/blob/main/dom/xml/resources/XMLPrettyPrint.xsl),IE浏览器也有。正当我为XSLT的功能感到强大时,谷歌AI随后提到,[Chrome浏览器决定弃用XSLT](https://developer.chrome.com/docs/web-platform/deprecating-xslt),所以以后不要再用XSLT了😰…… |
|||
我给我的订阅文件加美化功能才半年,怎么就要不能用了?XSLT出现这么多年都还能用,结果等我加上就要废弃了?当时为了增加这个功能,还是费了不少劲的,怎么能让谷歌说没就没?于是我就开始对这件事进行了调查。 |
|||
|
|||
# Google杀死了XSLT |
|||
从上面Chrome的弃用XSLT文档中,可以发现,这件事的始作俑者是[Mason Freed](https://github.com/mfreed7),他在WHATWG中发起了一个[Issue](https://github.com/whatwg/html/issues/11523),因为XSLT用的人很少,以及实现XSLT的库很老而且容易出漏洞,所以建议把XSLT从Web标准中删除。在这个Issue中可以发现,有很多人表示不满,毕竟这个功能对想要给自己订阅做美化的博主来说还是很有用的。为了对抗谷歌,还有人做了个网站: <https://xslt.rip> 。 |
|||
而且XSLT虽然用的人占比也许不高,但从总量上应该还是挺多的,除了用XSLT美化博客订阅的,甚至还有用[XSLT作为博客框架的](https://github.com/vgr-land/vgr-xslt-blog-framework),另外还有一些人提出[一部分政府网站也有使用XSLT](https://github.com/whatwg/html/issues/11582)。 |
|||
不过Freed看起来对这件事早有准备,他做了一个[Polyfill库](https://github.com/mfreed7/xslt_polyfill),通过WASM的方式让XSLT可以正常工作,为了方便大家使用这个库,我顺手给CDNJS发了个[PR](https://github.com/cdnjs/packages/pull/2118),以后可以用CDN引用它了。不过使用这个库的前提是需要在订阅中加一段引用JS的代码,像我博客中的Atom订阅,用的是[jekyll-feed](https://github.com/jekyll/jekyll-feed)插件,里面的格式都是写死的,就用不了了…… |
|||
只不过现在已经没办法阻止谷歌了……而且其他浏览器也表示会跟进,看来我们唯一能做的就是去适应了。 |
|||
|
|||
# 没有XSLT之后的美化方案 |
|||
## 纯CSS |
|||
虽然XSLT不能用,但不代表`xml-stylesheet`指令就不能用了,除了XSLT之外,`xml-stylesheet`同样可以引用CSS。只是似乎完全没见过用CSS美化订阅源的,也许是因为光用CSS能做到的事比较少吧,想用CSS给XML文档加链接之类的估计就做不到了。 |
|||
但目前能选择的也不多了,既然大家都没写过用CSS美化订阅源,那就让我来写一个吧!然而我并不会写😅……那就只好让AI来写了,我把需求说清楚之后,AI就写出来了:[feed.css](/assets/css/feed.css)。试了一下效果还挺不错的,我让AI写的这个版本无论是RSS还是Atom都可以使用,如果有人感兴趣可以拿去用。可惜我的Atom订阅因为用的是插件的原因用不了😭,只能加到用纯Liquid实现的RSS订阅上了。 |
|||
但用纯CSS的缺点也很明显,没办法操作文档的内容,像修改日期格式的就做不了了,而且也不能添加超链接……XML的标签本身对浏览器来说并没有内建的语义,正常情况下也没法让浏览器把某个标签当作超链接。那难道就没办法了吗? |
|||
## 混合XHTML |
|||
如果完全不能修改XML内容,那确实就没有办法了,但如果能修改XML的内容那还是有办法的,简单来说就是混入XHTML,事实上Freed编写的Polyfill库原理上也是利用了XHTML,只要在能作为XHTML的标签中添加XHTML的命名空间,那么浏览器就可以理解它的语义并渲染,像刚刚用纯CSS美化的订阅没有链接,那就可以在根元素中添加命名空间:`xmlns:xhtml="http://www.w3.org/1999/xhtml"`,然后在合适的位置写: |
|||
```xml |
|||
<xhtml:a href="https://example.com">Read more -></xhtml:a> |
|||
``` |
|||
就可以了。只是这样有个缺点,这样写的订阅文件不够“纯粹”,用验证器验证会显示“[Misplaced XHTML content](https://validator.w3.org/feed/docs/warning/MisplacedXHTMLContent.html)”警告。对有洁癖的人来说可能会有点难受😆。 |
|||
不过如果能接受这种“不纯粹”,那么其实`xml-stylesheet`指令也没必要了,`link`标签一样可以用,包括`script`也是,所以有人写了一个[不使用XSLT美化XML](https://github.com/dfabulich/style-xml-feeds-without-xslt)的库。 |
|||
只不过这种方法和XSLT相比还是有一些缺陷,要知道XSLT的本质是转换,是把XML转换为HTML,也就是说转出来的文档本质是HTML,所有的DOM操作都和操作HTML是完全相同的,但是在XML里混入XHTML标签就不一样了,它的本质依然是XML文档,只是嵌入了XHTML命名空间下的元素,所以相应的DOM操作会有一些不同。如果是自己写的纯JS可能还好,如果是用了jQuery之类假定DOM为HTML的库就会出现问题了,因此这也就是那个Polyfill库的局限性,用正常的XSLT执行`document.constructor`会显示`HTMLDocument`,而用这个Polyfill库执行完则是显示`XMLDocument`。因此,直接套用为浏览器原生XSLT编写的旧样式文件,就有可能会出问题,但如果要考虑改XSLT的话那还不如重新写JS,然后用XHTML引入呢。 |
|||
|
|||
# 感想 |
|||
虽然有一些技术会因为各种各样的原因消失,但这不代表我们就要妥协一些东西,总有一些不同的技术可以解决相同的问题,所以我们只需要用其他的技术去实现就好了。不过这也是没办法的事情,毕竟没人能改变浏览器厂商们的决策啊😂。 |
|||
@ -1,388 +1,381 @@ |
|||
async function sha(str) { |
|||
const encoder = new TextEncoder(); |
|||
const data = encoder.encode(str); |
|||
const hashBuffer = await crypto.subtle.digest("SHA-256", data); |
|||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
|||
const hashHex = hashArray |
|||
.map((b) => b.toString(16).padStart(2, "0")) |
|||
.join(""); // convert bytes to hex string
|
|||
return hashHex; |
|||
} |
|||
async function md5(str) { |
|||
const encoder = new TextEncoder(); |
|||
const data = encoder.encode(str); |
|||
const hashBuffer = await crypto.subtle.digest("MD5", data); |
|||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
|||
const hashHex = hashArray |
|||
.map((b) => b.toString(16).padStart(2, "0")) |
|||
.join(""); // convert bytes to hex string
|
|||
return hashHex; |
|||
} |
|||
const encoder = new TextEncoder(); |
|||
const data = encoder.encode(str); |
|||
const hashBuffer = await crypto.subtle.digest("SHA-256", data); |
|||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
|||
const hashHex = hashArray |
|||
.map((b) => b.toString(16).padStart(2, "0")) |
|||
.join(""); // convert bytes to hex string
|
|||
return hashHex; |
|||
} |
|||
async function md5(str) { |
|||
const encoder = new TextEncoder(); |
|||
const data = encoder.encode(str); |
|||
const hashBuffer = await crypto.subtle.digest("MD5", data); |
|||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
|||
const hashHex = hashArray |
|||
.map((b) => b.toString(16).padStart(2, "0")) |
|||
.join(""); // convert bytes to hex string
|
|||
return hashHex; |
|||
} |
|||
|
|||
export default { |
|||
async fetch(request, env, ctx) { |
|||
const db = env.blog_summary.withSession(); |
|||
const counter_db = env.blog_counter |
|||
const url = new URL(request.url); |
|||
const query = decodeURIComponent(url.searchParams.get('id')); |
|||
var commonHeader = { |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
export default { |
|||
async fetch(request, env, ctx) { |
|||
const db = env.blog_summary.withSession(); |
|||
const counter_db = env.blog_counter |
|||
const url = new URL(request.url); |
|||
const query = decodeURIComponent(url.searchParams.get('id')); |
|||
var commonHeader = { |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
} |
|||
if (url.pathname.startsWith("/ai_chat")) { |
|||
// 获取请求中的文本数据
|
|||
if (!(request.headers.get('accept') || '').includes('text/event-stream')) { |
|||
return Response.redirect("https://mabbs.github.io", 302); |
|||
} |
|||
if (url.pathname.startsWith("/ai_chat")) { |
|||
// 获取请求中的文本数据
|
|||
if (!(request.headers.get('accept') || '').includes('text/event-stream')) { |
|||
return Response.redirect("https://mabbs.github.io", 302); |
|||
// const req = await request.formData();
|
|||
let questsion = decodeURIComponent(url.searchParams.get('info')) |
|||
let notes = []; |
|||
let refer = []; |
|||
let contextMessage; |
|||
if (query != "null") { |
|||
try { |
|||
const result = String(await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content")); |
|||
contextMessage = result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} catch (e) { |
|||
console.error({ |
|||
message: e.message |
|||
}); |
|||
contextMessage = "无法获取到文章内容"; |
|||
} |
|||
// const req = await request.formData();
|
|||
let questsion = decodeURIComponent(url.searchParams.get('info')) |
|||
let notes = []; |
|||
let refer = []; |
|||
let contextMessage; |
|||
if (query != "null") { |
|||
try { |
|||
const result = String(await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content")); |
|||
contextMessage = result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} catch (e) { |
|||
console.error({ |
|||
message: e.message |
|||
}); |
|||
contextMessage = "无法获取到文章内容"; |
|||
} |
|||
notes.push("content"); |
|||
} else { |
|||
try { |
|||
const response = await env.AI.run( |
|||
"@cf/meta/m2m100-1.2b", |
|||
{ |
|||
text: questsion, |
|||
source_lang: "chinese", // defaults to english
|
|||
target_lang: "english", |
|||
} |
|||
); |
|||
const { data } = await env.AI.run( |
|||
"@cf/baai/bge-base-en-v1.5", |
|||
{ |
|||
text: response.translated_text, |
|||
} |
|||
); |
|||
let embeddings = data[0]; |
|||
let { matches } = await env.mayx_index.query(embeddings, { topK: 5 }); |
|||
for (let i = 0; i < matches.length; i++) { |
|||
if (matches[i].score > 0.6) { |
|||
notes.push(await db.prepare( |
|||
"SELECT summary FROM blog_summary WHERE id = ?1" |
|||
).bind(matches[i].id).first("summary")); |
|||
refer.push(matches[i].id); |
|||
} |
|||
}; |
|||
contextMessage = notes.length |
|||
? `Mayx的博客相关文章摘要:\n${notes.map(note => `- ${note}`).join("\n")}` |
|||
: "" |
|||
} catch (e) { |
|||
console.error({ |
|||
message: e.message |
|||
}); |
|||
contextMessage = "无法获取到文章内容"; |
|||
} |
|||
notes.push("content"); |
|||
} else { |
|||
try { |
|||
const response = await env.AI.run( |
|||
"@cf/meta/m2m100-1.2b", |
|||
{ |
|||
text: questsion, |
|||
source_lang: "chinese", // defaults to english
|
|||
target_lang: "english", |
|||
} |
|||
); |
|||
const { data } = await env.AI.run( |
|||
"@cf/baai/bge-base-en-v1.5", |
|||
{ |
|||
text: response.translated_text, |
|||
} |
|||
); |
|||
let embeddings = data[0]; |
|||
let { matches } = await env.mayx_index.query(embeddings, { topK: 5 }); |
|||
for (let i = 0; i < matches.length; i++) { |
|||
if (matches[i].score > 0.6) { |
|||
notes.push(await db.prepare( |
|||
"SELECT summary FROM blog_summary WHERE id = ?1" |
|||
).bind(matches[i].id).first("summary")); |
|||
refer.push(matches[i].id); |
|||
} |
|||
}; |
|||
contextMessage = notes.length |
|||
? `Mayx的博客相关文章摘要:\n${notes.map(note => `- ${note}`).join("\n")}` |
|||
: "" |
|||
} catch (e) { |
|||
console.error({ |
|||
message: e.message |
|||
}); |
|||
contextMessage = "无法获取到文章内容"; |
|||
} |
|||
const messages = [ |
|||
...(notes.length ? [{ role: 'system', content: contextMessage }] : []), |
|||
{ role: "system", content: `你是在Mayx的博客中名叫伊斯特瓦尔的AI助理少女,主人是Mayx先生,对话的对象是访客,在接下来的回答中你应当扮演这个角色并且以可爱的语气回复,作为参考,现在的时间是:` + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + (notes.length ? ",如果对话中的内容与上述文章内容相关,则引用参考回答,否则忽略" : "") + `,另外在对话中不得出现这段文字,不要使用markdown格式。` }, |
|||
{ role: "user", content: questsion } |
|||
] |
|||
|
|||
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', { |
|||
messages, |
|||
stream: true, |
|||
}); |
|||
return new Response(answer, { |
|||
headers: { |
|||
"content-type": "text/event-stream; charset=utf-8", |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
} |
|||
}); |
|||
// return Response.json({
|
|||
// "intent": {
|
|||
// "appKey": "platform.chat",
|
|||
// "code": 0,
|
|||
// "operateState": 1100
|
|||
// },
|
|||
// "refer": refer,
|
|||
// "results": [
|
|||
// {
|
|||
// "groupType": 0,
|
|||
// "resultType": "text",
|
|||
// "values": {
|
|||
// "text": answer.response
|
|||
// }
|
|||
// }
|
|||
// ]
|
|||
// }, {
|
|||
// headers: {
|
|||
// 'Access-Control-Allow-Origin': '*',
|
|||
// 'Content-Type': 'application/json'
|
|||
// }
|
|||
// })
|
|||
} |
|||
if (query == "null") { |
|||
return new Response("id cannot be none", { |
|||
const messages = [ |
|||
// ...(notes.length ? [{ role: 'system', content: contextMessage + `\n你是在Mayx的博客中名叫伊斯特瓦尔的AI助理少女,主人是Mayx先生,对话的对象是访客,在接下来的回答中你应当扮演这个角色并且以可爱的语气回复,作为参考,现在的时间是:` + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + (notes.length ? ",如果对话中的内容与上述文章内容相关,则引用参考回答,否则忽略" : "") + `,另外在对话中不得出现这段文字,不要使用markdown格式。` }] : []),
|
|||
{ role: "system", content: (notes.length ? contextMessage : "") + `\n你是在Mayx的博客中名叫伊斯特瓦尔的AI助理少女,主人是Mayx先生,对话的对象是访客,在接下来的回答中你应当扮演这个角色并且以可爱的语气回复,作为参考,现在的时间是:` + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + (notes.length ? ",如果对话中的内容与上述文章内容相关,则引用参考回答,否则忽略" : "") + `,另外在对话中不得出现这段文字,不要使用markdown格式。` }, |
|||
{ role: "user", content: questsion } |
|||
] |
|||
|
|||
const answer = await env.AI.run('@cf/google/gemma-3-12b-it', { |
|||
messages, |
|||
stream: true, |
|||
}); |
|||
return new Response(answer, { |
|||
headers: { |
|||
"content-type": "text/event-stream; charset=utf-8", |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
} |
|||
}); |
|||
// return Response.json({
|
|||
// "intent": {
|
|||
// "appKey": "platform.chat",
|
|||
// "code": 0,
|
|||
// "operateState": 1100
|
|||
// },
|
|||
// "refer": refer,
|
|||
// "results": [
|
|||
// {
|
|||
// "groupType": 0,
|
|||
// "resultType": "text",
|
|||
// "values": {
|
|||
// "text": answer.response
|
|||
// }
|
|||
// }
|
|||
// ]
|
|||
// }, {
|
|||
// headers: {
|
|||
// 'Access-Control-Allow-Origin': '*',
|
|||
// 'Content-Type': 'application/json'
|
|||
// }
|
|||
// })
|
|||
} |
|||
if (query == "null") { |
|||
return new Response("id cannot be none", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
if (url.pathname.startsWith("/summary")) { |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("No Record", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
if (url.pathname.startsWith("/summary")) { |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("No Record", { |
|||
headers: commonHeader |
|||
}); |
|||
|
|||
const messages = [ |
|||
{ |
|||
role: "system", content: ` |
|||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。 |
|||
技能 |
|||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。 |
|||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。 |
|||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。 |
|||
约束 |
|||
输出内容必须以中文进行。 |
|||
必须确保摘要内容准确反映原文章的主旨和重点。 |
|||
尊重原文的观点,不能进行歪曲或误导。 |
|||
在摘要中明确区分事实与作者的意见或分析。 |
|||
提示 |
|||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。 |
|||
格式 |
|||
你的回答格式应该如下: |
|||
这篇文章介绍了<这里是内容> |
|||
` },
|
|||
{ |
|||
role: "user", content: result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} |
|||
] |
|||
|
|||
const messages = [ |
|||
{ |
|||
role: "system", content: ` |
|||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。 |
|||
技能 |
|||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。 |
|||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。 |
|||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。 |
|||
约束 |
|||
输出内容必须以中文进行。 |
|||
必须确保摘要内容准确反映原文章的主旨和重点。 |
|||
尊重原文的观点,不能进行歪曲或误导。 |
|||
在摘要中明确区分事实与作者的意见或分析。 |
|||
提示 |
|||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。 |
|||
格式 |
|||
你的回答格式应该如下: |
|||
这篇文章介绍了<这里是内容> |
|||
` },
|
|||
{ |
|||
role: "user", content: result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} |
|||
] |
|||
const stream = await env.AI.run('@cf/google/gemma-3-12b-it', { |
|||
messages, |
|||
stream: true, |
|||
}); |
|||
|
|||
const stream = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', { |
|||
messages, |
|||
stream: true, |
|||
return new Response(stream, { |
|||
headers: { |
|||
"content-type": "text/event-stream; charset=utf-8", |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
} |
|||
}); |
|||
} else if (url.pathname.startsWith("/get_summary")) { |
|||
const orig_sha = decodeURIComponent(url.searchParams.get('sign')); |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
|
|||
return new Response(stream, { |
|||
headers: { |
|||
"content-type": "text/event-stream; charset=utf-8", |
|||
'Access-Control-Allow-Origin': '*', |
|||
'Access-Control-Allow-Methods': "*", |
|||
'Access-Control-Allow-Headers': "*", |
|||
'Access-Control-Max-Age': '86400', |
|||
} |
|||
} |
|||
let result_sha = await sha(result); |
|||
if (result_sha != orig_sha) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} else if (url.pathname.startsWith("/get_summary")) { |
|||
const orig_sha = decodeURIComponent(url.searchParams.get('sign')); |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
let result_sha = await sha(result); |
|||
if (result_sha != orig_sha) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} else { |
|||
let resp = await db.prepare( |
|||
"SELECT summary FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("summary"); |
|||
if (!resp) { |
|||
const messages = [ |
|||
{ |
|||
role: "system", content: ` |
|||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。 |
|||
技能 |
|||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。 |
|||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。 |
|||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。 |
|||
约束 |
|||
输出内容必须以中文进行。 |
|||
必须确保摘要内容准确反映原文章的主旨和重点。 |
|||
尊重原文的观点,不能进行歪曲或误导。 |
|||
在摘要中明确区分事实与作者的意见或分析。 |
|||
提示 |
|||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。 |
|||
格式 |
|||
你的回答格式应该如下: |
|||
这篇文章介绍了<这里是内容> |
|||
` },
|
|||
{ |
|||
role: "user", content: result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} |
|||
] |
|||
} else { |
|||
let resp = await db.prepare( |
|||
"SELECT summary FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("summary"); |
|||
if (!resp) { |
|||
const messages = [ |
|||
{ |
|||
role: "system", content: ` |
|||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。 |
|||
技能 |
|||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。 |
|||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。 |
|||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。 |
|||
约束 |
|||
输出内容必须以中文进行。 |
|||
必须确保摘要内容准确反映原文章的主旨和重点。 |
|||
尊重原文的观点,不能进行歪曲或误导。 |
|||
在摘要中明确区分事实与作者的意见或分析。 |
|||
提示 |
|||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。 |
|||
格式 |
|||
你的回答格式应该如下: |
|||
这篇文章介绍了<这里是内容> |
|||
` },
|
|||
{ |
|||
role: "user", content: result.length > 6000 ? |
|||
result.slice(0, 3000) + result.slice(-3000) : |
|||
result.slice(0, 6000) |
|||
} |
|||
] |
|||
|
|||
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', { |
|||
messages, |
|||
stream: false, |
|||
}); |
|||
resp = answer.response |
|||
await db.prepare("UPDATE blog_summary SET summary = ?1 WHERE id = ?2") |
|||
.bind(resp, query).run(); |
|||
} |
|||
let is_vec = await db.prepare( |
|||
"SELECT `is_vec` FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("is_vec"); |
|||
if (is_vec == 0) { |
|||
const response = await env.AI.run( |
|||
"@cf/meta/m2m100-1.2b", |
|||
{ |
|||
text: resp, |
|||
source_lang: "chinese", // defaults to english
|
|||
target_lang: "english", |
|||
} |
|||
); |
|||
const { data } = await env.AI.run( |
|||
"@cf/baai/bge-base-en-v1.5", |
|||
{ |
|||
text: response.translated_text, |
|||
} |
|||
); |
|||
let embeddings = data[0]; |
|||
await env.mayx_index.upsert([{ |
|||
id: query, |
|||
values: embeddings |
|||
}]); |
|||
await db.prepare("UPDATE blog_summary SET is_vec = 1 WHERE id = ?1") |
|||
.bind(query).run(); |
|||
} |
|||
return new Response(resp, { |
|||
headers: commonHeader |
|||
const answer = await env.AI.run('@cf/google/gemma-3-12b-it', { |
|||
messages, |
|||
stream: false, |
|||
}); |
|||
resp = answer.response |
|||
await db.prepare("UPDATE blog_summary SET summary = ?1 WHERE id = ?2") |
|||
.bind(resp, query).run(); |
|||
} |
|||
let is_vec = await db.prepare( |
|||
"SELECT `is_vec` FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("is_vec"); |
|||
if (is_vec == 0) { |
|||
const response = await env.AI.run( |
|||
"@cf/meta/m2m100-1.2b", |
|||
{ |
|||
text: resp, |
|||
source_lang: "chinese", // defaults to english
|
|||
target_lang: "english", |
|||
} |
|||
); |
|||
const { data } = await env.AI.run( |
|||
"@cf/baai/bge-base-en-v1.5", |
|||
{ |
|||
text: response.translated_text, |
|||
} |
|||
); |
|||
let embeddings = data[0]; |
|||
await env.mayx_index.upsert([{ |
|||
id: query, |
|||
values: embeddings |
|||
}]); |
|||
await db.prepare("UPDATE blog_summary SET is_vec = 1 WHERE id = ?1") |
|||
.bind(query).run(); |
|||
} |
|||
} else if (url.pathname.startsWith("/is_uploaded")) { |
|||
const orig_sha = decodeURIComponent(url.searchParams.get('sign')); |
|||
return new Response(resp, { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
} else if (url.pathname.startsWith("/is_uploaded")) { |
|||
const orig_sha = decodeURIComponent(url.searchParams.get('sign')); |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
let result_sha = await sha(result); |
|||
if (result_sha != orig_sha) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} else { |
|||
return new Response("yes", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
} else if (url.pathname.startsWith("/upload_blog")) { |
|||
if (request.method == "POST") { |
|||
const data = await request.text(); |
|||
let result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
let result_sha = await sha(result); |
|||
if (result_sha != orig_sha) { |
|||
return new Response("no", { |
|||
headers: commonHeader |
|||
}); |
|||
} else { |
|||
return new Response("yes", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
} else if (url.pathname.startsWith("/upload_blog")) { |
|||
if (request.method == "POST") { |
|||
const data = await request.text(); |
|||
let result = await db.prepare( |
|||
await db.prepare("INSERT INTO blog_summary(id, content) VALUES (?1, ?2)") |
|||
.bind(query, data).run(); |
|||
result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
if (!result) { |
|||
await db.prepare("INSERT INTO blog_summary(id, content) VALUES (?1, ?2)") |
|||
.bind(query, data).run(); |
|||
result = await db.prepare( |
|||
"SELECT content FROM blog_summary WHERE id = ?1" |
|||
).bind(query).first("content"); |
|||
} |
|||
if (result != data) { |
|||
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL, is_vec = 0 WHERE id = ?2") |
|||
.bind(data, query).run(); |
|||
} |
|||
return new Response("OK", { |
|||
headers: commonHeader |
|||
}); |
|||
} else { |
|||
return new Response("need post", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
} else if (url.pathname.startsWith("/count_click")) { |
|||
let id_md5 = await md5(query); |
|||
let count = await counter_db.prepare("SELECT `counter` FROM `counter` WHERE `url` = ?1") |
|||
.bind(id_md5).first("counter"); |
|||
if (url.pathname.startsWith("/count_click_add")) { |
|||
if (!count) { |
|||
await counter_db.prepare("INSERT INTO `counter` (`url`, `counter`) VALUES (?1, 1)") |
|||
.bind(id_md5).run(); |
|||
count = 1; |
|||
} else { |
|||
count += 1; |
|||
await counter_db.prepare("UPDATE `counter` SET `counter` = ?1 WHERE `url` = ?2") |
|||
.bind(count, id_md5).run(); |
|||
} |
|||
} |
|||
if (!count) { |
|||
count = 0; |
|||
if (result != data) { |
|||
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL, is_vec = 0 WHERE id = ?2") |
|||
.bind(data, query).run(); |
|||
} |
|||
return new Response(count, { |
|||
return new Response("OK", { |
|||
headers: commonHeader |
|||
}); |
|||
} else if (url.pathname.startsWith("/suggest")) { |
|||
let resp = []; |
|||
let update_time = url.searchParams.get('update'); |
|||
if (update_time) { |
|||
let result = await env.mayx_index.getByIds([ |
|||
query |
|||
]); |
|||
if (result.length) { |
|||
let cache = await db.prepare("SELECT `id`, `suggest`, `suggest_update` FROM `blog_summary` WHERE `id` = ?1") |
|||
.bind(query).first(); |
|||
if (!cache.id) { |
|||
return Response.json(resp, { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
if (update_time != cache.suggest_update) { |
|||
resp = await env.mayx_index.query(result[0].values, { topK: 6 }); |
|||
resp = resp.matches; |
|||
resp.splice(0, 1); |
|||
await db.prepare("UPDATE `blog_summary` SET `suggest_update` = ?1, `suggest` = ?2 WHERE `id` = ?3") |
|||
.bind(update_time, JSON.stringify(resp), query).run(); |
|||
commonHeader["x-suggest-cache"] = "miss" |
|||
} else { |
|||
resp = JSON.parse(cache.suggest); |
|||
commonHeader["x-suggest-cache"] = "hit" |
|||
} |
|||
} else { |
|||
return new Response("need post", { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
} else if (url.pathname.startsWith("/count_click")) { |
|||
let id_md5 = await md5(query); |
|||
let count = await counter_db.prepare("SELECT `counter` FROM `counter` WHERE `url` = ?1") |
|||
.bind(id_md5).first("counter"); |
|||
if (url.pathname.startsWith("/count_click_add")) { |
|||
if (!count) { |
|||
await counter_db.prepare("INSERT INTO `counter` (`url`, `counter`) VALUES (?1, 1)") |
|||
.bind(id_md5).run(); |
|||
count = 1; |
|||
} else { |
|||
count += 1; |
|||
await counter_db.prepare("UPDATE `counter` SET `counter` = ?1 WHERE `url` = ?2") |
|||
.bind(count, id_md5).run(); |
|||
} |
|||
} |
|||
if (!count) { |
|||
count = 0; |
|||
} |
|||
return new Response(count, { |
|||
headers: commonHeader |
|||
}); |
|||
} else if (url.pathname.startsWith("/suggest")) { |
|||
let resp = []; |
|||
let update_time = url.searchParams.get('update'); |
|||
if (update_time) { |
|||
let result = await env.mayx_index.getByIds([ |
|||
query |
|||
]); |
|||
if (result.length) { |
|||
let cache = await db.prepare("SELECT `id`, `suggest`, `suggest_update` FROM `blog_summary` WHERE `id` = ?1") |
|||
.bind(query).first(); |
|||
if (!cache.id) { |
|||
return Response.json(resp, { |
|||
headers: commonHeader |
|||
}); |
|||
} |
|||
if (update_time != cache.suggest_update) { |
|||
resp = await env.mayx_index.query(result[0].values, { topK: 6 }); |
|||
resp = resp.matches; |
|||
resp.splice(0, 1); |
|||
await db.prepare("UPDATE `blog_summary` SET `suggest_update` = ?1, `suggest` = ?2 WHERE `id` = ?3") |
|||
.bind(update_time, JSON.stringify(resp), query).run(); |
|||
commonHeader["x-suggest-cache"] = "miss" |
|||
} else { |
|||
resp = JSON.parse(cache.suggest); |
|||
commonHeader["x-suggest-cache"] = "hit" |
|||
} |
|||
resp = resp.map(respObj => { |
|||
respObj.id = encodeURI(respObj.id); |
|||
return respObj; |
|||
}); |
|||
} |
|||
return Response.json(resp, { |
|||
headers: commonHeader |
|||
resp = resp.map(respObj => { |
|||
respObj.id = encodeURI(respObj.id); |
|||
return respObj; |
|||
}); |
|||
} else if (url.pathname.startsWith("/***")) { |
|||
let resp = await db.prepare("SELECT `id`, `summary` FROM `blog_summary` WHERE `suggest_update` IS NOT NULL").run(); |
|||
const resultObject = resp.results.reduce((acc, item) => { |
|||
acc[item.id] = item.summary; // 将每个项的 id 作为键,summary 作为值
|
|||
return acc; |
|||
}, {}); // 初始值为空对象
|
|||
return Response.json(resultObject); |
|||
} else { |
|||
return Response.redirect("https://mabbs.github.io", 302) |
|||
} |
|||
return Response.json(resp, { |
|||
headers: commonHeader |
|||
}); |
|||
} else { |
|||
return Response.redirect("https://mabbs.github.io", 302) |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue