mirror of
https://github.com/Mabbs/mabbs.github.io
synced 2026-02-24 02:53:43 +01:00
Compare commits
8 Commits
e9ac9bf1df
...
2ab6982684
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab6982684 | ||
|
|
172882a99e | ||
|
|
d69f175fee | ||
|
|
9760f9eb4d | ||
|
|
1553183d31 | ||
|
|
0ad9008f3e | ||
|
|
03a2f1fdf9 | ||
|
|
da73615b73 |
@@ -171,5 +171,8 @@
|
||||
"/2025/08/10/tilde.html": "这篇文章介绍了作者在Tilde社区的体验,这是一类基于类Unix环境的公共服务器社区,类似于家目录,提供预装的软件、开发环境和公共服务,如聊天室、邮件、BBS论坛等,强调了社区的互动性和共享精神。作者通过申请、审核过程加入了几个社区,并详细描述了在这些社区中的个人主页、编程支持(如Gemini和Gopher协议)、博客发布、代码托管(Git支持)、CI/CD部署以及使用Git hooks自动化博客更新等功能。尽管作者受限于语言和工具使用体验,未能充分参与社区交流,但对社区学习新知识和丰富博客内容印象深刻。",
|
||||
"/2025/09/01/quine.html": "这篇文章主要介绍了作者在博客部署过程中,对ZIP Quine(自包含压缩包)和自产生程序的探索过程。作者起初想利用压缩包实现离线浏览,但遇到了压缩包不包含自身的问题。随后,作者回顾了ZIP Quine的原理,如droste.zip,以及如何通过DEFLATE压缩算法的LZ77编码实现自包含。作者尝试了Russ Cox的方案,但发现由于压缩格式限制,实际操作中存在数据容量的限制,无法存下整个博客。尽管如此,作者还是研究了嵌套循环的ZIP Quine,如Ruben Van Mello的论文中所描述的,尽管空间仍然有限。探索过程中,作者还学习了自产生程序(Quine)的概念,包括其实现原理和各种编程语言中的例子。作者最后感慨,探索过程中的收获比原本的目标更重要。",
|
||||
"/2025/10/12/recover.html": "这篇文章讲述了作者通过GitHub的Fork特性找回一个被删除的Brainfuck可视化演示仓库的经历。由于原仓库和作者主页都已消失,作者推测GitHub在Fork时会共享对象库,只要有任意一个Fork仓库存在,GitHub就会保留所有对象,从而可以通过找到一个Fork仓库的最新提交Hash值来还原目标仓库。作者通过Linux内核仓库的Fork进行验证,随后在互联网档案馆上找到目标仓库的Fork以及其Hash值,最终通过Git命令将本地仓库的HEAD指针指向目标提交,成功恢复了该仓库的代码,并将其部署到自己的GitHub Pages上。最后,作者发现Software Heritage组织会保存所有代码,因此在遇到类似情况时可以直接通过该平台进行查找。",
|
||||
"/2025/11/01/mirrors.html": "这篇文章讲述了作者为了提高博客的可靠性,探索利用被滥用的Git平台进行博客镜像的想法和实践。作者发现一些Git实例存在大量空仓库和异常用户,怀疑是SEO公司滥用,因此决定利用这些平台进行博客镜像备份,以应对平台倒闭或数据丢失的风险。作者选择Gitea和Forgejo平台作为目标,编写脚本自动注册账号并导入博客仓库,实现了自动化镜像分发。作者也意识到此类平台的稳定性存在不确定性,并思考了“量”和“质”两种方式确保博客永恒性的优劣,最终认为建立一个活跃的、自动执行维护操作的网络可能更有效。文章最后展示了作者创建的Git镜像列表,并表达了对博客永恒性的思考。"
|
||||
"/2025/11/01/mirrors.html": "这篇文章讲述了作者为了提高博客的可靠性,探索利用被滥用的Git平台进行博客镜像的想法和实践。作者发现一些Git实例存在大量空仓库和异常用户,怀疑是SEO公司滥用,因此决定利用这些平台进行博客镜像备份,以应对平台倒闭或数据丢失的风险。作者选择Gitea和Forgejo平台作为目标,编写脚本自动注册账号并导入博客仓库,实现了自动化镜像分发。作者也意识到此类平台的稳定性存在不确定性,并思考了“量”和“质”两种方式确保博客永恒性的优劣,最终认为建立一个活跃的、自动执行维护操作的网络可能更有效。文章最后展示了作者创建的Git镜像列表,并表达了对博客永恒性的思考。",
|
||||
"/2025/12/01/linux.html": "这篇文章介绍了在浏览器中运行Linux的各种方法,从最初的纯JS虚拟机JSLinux,到后来的WASM虚拟机如v86、WebVM、WebCM,再到容器化方案container2wasm,以及直接将Linux内核编译为WASM的方案。作者详细对比了这些方案的优缺点,包括性能、兼容性、功能和开发难度。文章还提到了模仿Linux环境的WebContainers和JupyterLite,并最终认为虚拟机方案更靠谱,但对WASM的未来充满期待。作者最后表示,博客上添加类似功能的计划还在考虑中,目前主要分享了各种方法的探索过程。",
|
||||
"/2026/01/01/summary.html": "这篇文章介绍了作者对2025年的年终总结,主要表达了对自身状态的担忧和对未来的不确定感。作者认为自己在记忆和思考能力方面有所下滑,稳定性较低,且未能抓住资产保值的机会。同时,文章也记录了AI技术的飞速发展,以及自己博客内容与时代脱节的现象。尽管对未来感到迷茫,作者仍然抱有一丝希望,期望在2026年做出正确的选择,避免陷入危险。",
|
||||
"/2026/02/08/xslt.html": "这篇文章讲述了Google计划弃用XSLT技术,以及作者对这一决定的调查和应对方案。Google基于XSLT用户占比低、库存在漏洞等原因,建议将其从Web标准中删除。作者发现许多用户依赖XSLT进行博客订阅美化,甚至将其作为博客框架。为了对抗这一趋势,有人创建了网站https://xslt.rip,并开发了Polyfill库,通过WASM方式保持XSLT功能。虽然Polyfill库需要额外引用JS代码,但作者已将其提交至CDNJS。随后,作者探讨了替代方案,包括使用纯CSS美化订阅源(由AI生成feed.css),以及混合XHTML的方式,通过添加XHTML命名空间来实现链接等功能,但这种方法会产生“不纯粹”的警告。文章最后总结,技术可能会消失,但总有其他技术可以解决问题,并强调了适应浏览器厂商决策的重要性。"
|
||||
}
|
||||
@@ -18,6 +18,6 @@ Vullfin的博客,https://blog.vull.top/,https://blog.vull.top/atom.xml,Vullfin's
|
||||
Lanke's blog,https://blog.blueke.top/,https://blog.blueke.top/rss.xml,请为一切不真实之物骄傲,因为我们高于这个世界!
|
||||
时光流·言,https://www.hansjack.com/,https://www.hansjack.com/feed/,个人博客,持续分享网站部署实战经验、精选书评解读和生活观察手记。 这里提供可复用的技术教程、深度阅读指南和真实生活洞察,与技术爱好者一起进步......
|
||||
Pinpe 的云端,https://pinpe.top/,https://pinpe.top/rss.xml,一个属于自己的云朵。
|
||||
Chise Hachiroku,https://chise.hachiroku.com/,https://chise.hachiroku.com/zh/feed/,向明日的辉迹,干杯!
|
||||
Chise Hachiroku,https://chise.hachiroku.com/zh/,https://chise.hachiroku.com/zh/feed/,向明日的辉迹,干杯!
|
||||
映屿,https://www.glowisle.me/,https://www.glowisle.me/atom.xml,关于互联网、书籍、生活琐事以及那些一闪而过的念头
|
||||
东东,https://nihaha.com/,https://nihaha.com/feed/,城市与信仰
|
||||
Restent's Notebook,https://blog.gxres.net/,https://blog.gxres.net/atom.xml,不前沿技术分享
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ mirrors:
|
||||
- https://mayx.gitlab.io/
|
||||
- https://mayx.pages.dev/
|
||||
- https://mayx.eu.org/
|
||||
- https://mayx.envs.sh/
|
||||
- https://mayx.envs.net/
|
||||
- https://mayx.frama.io/
|
||||
- https://mayx.surge.sh/
|
||||
@@ -33,19 +32,23 @@ repos:
|
||||
- https://framagit.org/mayx/mayx.frama.io
|
||||
- https://salsa.debian.org/mayx/mayx.pages.debian.net
|
||||
- https://codeberg.org/mayx/blog
|
||||
- https://pagure.io/mayx
|
||||
- https://git.gay/mayx/mayx
|
||||
- https://gitea.com/mayx/mayx
|
||||
- https://gitgud.io/mayx/mayx
|
||||
- https://git.sr.ht/~mayx/mayx
|
||||
- https://git.launchpad.net/mayx
|
||||
- https://gin.g-node.org/mayx/blog
|
||||
- https://git.disroot.org/mayx/mayx
|
||||
- https://bitbucket.org/unmayx/mayx
|
||||
- https://sourcecraft.dev/mayx/mayx
|
||||
- https://gitflic.ru/project/mayx/blog
|
||||
- https://tangled.org/mayx.tngl.sh/blog/
|
||||
- https://gitee.com/mabbs/mabbs
|
||||
- https://cnb.cool/unmayx/mayx
|
||||
- https://atomgit.com/mayx/blog
|
||||
- https://sourceforge.net/projects/mayx/
|
||||
- https://dev.azure.com/unmayx/_git/Mayx
|
||||
static:
|
||||
- https://mayx.nekoweb.org/
|
||||
- https://mayx.neocities.org/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% if page.layout == "xslt" %}<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml-stylesheet type="text/xml" href="/feed.xslt.xml"?>
|
||||
<?xml-stylesheet type="text/css" href="/assets/css/xslt.css"?>
|
||||
<xsl:stylesheet
|
||||
version="3.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
|
||||
38
_posts/2025-12-01-linux.md
Normal file
38
_posts/2025-12-01-linux.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
layout: post
|
||||
title: 在浏览器中运行Linux的各种方法
|
||||
tags: [浏览器, Linux, 虚拟机, WASM]
|
||||
---
|
||||
|
||||
浏览器已经无所不能了!<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间跟网友交流时,有人展示了他博客里的一个Linux终端模拟项目:[jsnix](https://github.com/Erzbir/jsnix),看起来挺有意思的,里面甚至还藏了一个CTF。不过我感觉他这个终端和博客本身并没有真正联动起来,本质上只是一个模拟了Linux Shell行为的交互界面。除此之外我还发现了另一个风格类似的[个人主页](https://github.com/Luyoung0001/myWebsite),它虽然也走了终端风格,但功能更简单,还原度也不算高。不过它至少和博客内容做了一些基础联动——尽管目前也只是做到列出文章这种程度😂,当然有这类功能的博客应该也不少,只是我发现的不太多……于是我就想,不如我也给自己的博客加一个类似的“命令行访问”功能,应该会很有趣。当然如果真要做的话,我肯定不会满足于只实现几个模拟指令——既然要做,就要追求真实感,至少得在浏览器上运行真实的Linux终端,才不会让人觉得出戏吧😋。
|
||||
|
||||
# 在浏览器中运行Linux
|
||||
## 虚拟机方案
|
||||
### 纯JS虚拟机
|
||||
要说到在浏览器上运行Linux,最先想到的应该就是[Fabrice Bellard](https://bellard.org)大神写的[JSLinux](https://bellard.org/jslinux/)吧,这可能是第一个在浏览器中实现的虚拟机(毕竟是最强虚拟机QEMU的作者编写的)。现在他的个人主页中展示的这个版本是WASM版本,而他最早写的是纯JS实现的。那个JS实现的版本现在在GitHub上有一个[去混淆的版本](https://github.com/levskaya/jslinux-deobfuscated)可以用作学习和研究,于是我顺手Fork了一份在GitHub Pages上部署作为[演示](http://mabbs.github.io/jslinux/)。
|
||||
作为纯JS实现的x86虚拟机,性能估计是最差的,但相应的兼容性也最好,在Bellard当年写JSLinux的时候,还没有WASM这种东西呢,所以即使是在不支持WASM的IE11中,也可以正常运行。假如我想把它作为终端用在我的博客上,似乎也是个不错的选择,即使我完全看不懂代码,不知道如何实现JS和虚拟机的通信,它也预留了一个剪贴板设备,可以让我轻松地做到类似的事情,比如我在里面写个Bash脚本,通过它和外面的JS脚本联动来读取我的文章列表和内容,那也挺不错。
|
||||
当然Bellard用纯JS编写虚拟机也不是独一份,他实现了x86的虚拟机,相应的也有人用纯JS实现了RISC-V的虚拟机,比如[ANGEL](https://github.com/riscv-software-src/riscv-angel),看起来挺不错,所以同样也顺手[搭了一份](https://mabbs.github.io/riscv-angel/)。只不过它似乎用了一些更先进的语法,至少IE11上不能运行。
|
||||
另外还有一个比较知名的项目,叫做[jor1k](https://github.com/s-macke/jor1k),它模拟的是OpenRISC架构。只是这个架构目前已经过时,基本上没什么人用了,不过这里面还内置了几个演示的小游戏,看起来还挺有意思。
|
||||
除了这些之外,其实能在浏览器上运行的Linux也不一定是个网页,有一个叫做[LinuxPDF](https://github.com/ading2210/linuxpdf)的项目可以让Linux运行在PDF中,它的原理和JSLinux差不多,所以需要PDF阅读器支持JS,看它的介绍貌似只能在基于Chromium内核的浏览器中运行,而且因为安全问题在PDF中有很多功能不能用,所以它的速度甚至比JSLinux还要慢,功能还很少,因此它基本上只是个PoC,没什么太大的意义。
|
||||
### WASM虚拟机
|
||||
那还有别的方案吗?既然Bellard都选择放弃纯JS的JSLinux而选择了WASM,显然还有其他类似的项目,比如[v86](https://github.com/copy/v86),这也是一个能在浏览器中运行的x86虚拟机,不过因为使用了WASM和JIT技术,所以效率要比纯JS的JSLinux高得多。另外作为虚拟机,自然是不止能运行Linux,其他的系统也能运行,在示例中除了Linux之外还有DOS和Windows之类的系统,功能还挺强大,如果能自己做个系统镜像在博客里运行,似乎也是不错的选择。
|
||||
另外还有一个相对比较知名的叫[WebVM](https://github.com/leaningtech/webvm),从效果上来说和v86几乎没有区别,同样使用了WASM和JIT技术,也都只支持32位x86,然而它的虚拟化引擎CheerpX是闭源产品,既然和v86都拉不开差距,不知道是谁给他们的信心把它作为闭源产品😅。不过看它的说明文档,其相比于v86的主要区别是实现了Linux系统调用,考虑到它不能运行其他操作系统,而且Linux内核也不能更换,那我想它可能是类似于WSL1的那种实现方案,也许性能上会比v86好一些吧……只不过毕竟是闭源产品,不太清楚具体实现了。
|
||||
既然纯JS有RISC-V的虚拟机,WASM当然也有,比如[WebCM](https://github.com/edubart/webcm)。这个项目相比于其他的项目有个不太一样的地方,它把虚拟机、内核以及镜像打包成了一个单独的WASM文件……只是这样感觉并没有什么好处吧,改起来更加复杂了。
|
||||
以上这些虚拟机方案各有不同,但是想做一个自己的镜像相对来说还是有点困难,于是我又发现了另一个项目:[container2wasm](https://github.com/container2wasm/container2wasm),它可以让一个Docker镜像在浏览器中运行,当然实际实现其实和Docker并没有什么关系,本质还是虚拟机,只是制作镜像的时候可以直接用Docker镜像,方便了不少,但Docker镜像一般也都很大,所以第一次加载可能要下载很长时间。另外它还有一个优势,可以使用[Bochs](https://bochs.sourceforge.io/)运行x86_64的镜像,不像v86和WebVM只能模拟32位的x86(虽然Bochs的运行效率可能会差一些),而且可以使用WASI直接访问网络,不像以上几个项目如果需要访问网络需要用到中继服务。当然访问网络这个还是要受浏览器本身的跨域策略限制。总之从项目本身来说感觉也算是相当成熟了,尤其能用Docker镜像的话……我甚至可以考虑直接用[镜像](https://hub.docker.com/r/unmayx/mabbs)在线演示我曾经的[Mabbs](https://github.com/Mabbs/Mabbs.Project)项目😋。
|
||||
## 纯WASM方案
|
||||
其实想要在浏览器中运行Linux也不一定非得要用虚拟机,用虚拟机相当于是把其他指令集的机器码翻译为WASM,然后浏览器还得再翻译成宿主机CPU支持的指令集,然而WASM本身其实也算是一种指令集,各种编译型语言编写的程序也能编译出WASM的产物,比如[FFmpeg](https://github.com/ffmpegwasm/ffmpeg.wasm)。所以Linux内核也完全可以被编译成WASM,正好前段时间我看新闻说[Joel Severin](https://github.com/joelseverin)做了这么一个[项目](https://github.com/joelseverin/linux-wasm),对Linux内核做了一些修改使其可以被编译为WASM程序,我试了一下,貌似在Safari浏览器中不能正常工作……Chrome浏览器倒是没问题,不过即使这样用起来BUG也很多,随便执行几条命令就会冻结,体验不是很好。
|
||||
沿着这个项目,我又找到一个由[Thomas Stokes](https://github.com/tombl)制作的[项目](https://github.com/tombl/linux),和Joel的项目差不多,但我测了一下可以在Safari上运行,感觉这个项目更完善,不过之前那个项目上了新闻,所以⭐️数比这个更高😂。
|
||||
于是我把它复制了一份,在我的GitHub Pages上[部署](https://mabbs.github.io/linux/)了,但直接用仓库中的源代码会显示“Error: not cross origin isolated”,然而在Thomas自己部署的网站中可以正常打开,我看了一眼貌似是因为在GitHub Pages中没有[COOP和COEP响应头](https://web.dev/articles/coop-coep)导致的。Linux作为多任务操作系统来说,当然要运行多个进程,而Linux要管理它们就需要跨线程(Web Worker)读取内存的能力,所以用到了SharedArrayBuffer对象。不过由于CPU曾经出过“幽灵”漏洞,导致现代浏览器默认禁止使用SharedArrayBuffer对象,除非在服务器中配置COOP和COEP响应头才可以用,但是Joel的项目也是在GitHub Pages上运行的啊,为什么可以正常运行?看了源代码后才发现原来可以[用Service Worker作为反向代理](/2025/08/01/sw-proxy.html)来给请求的资源加上响应头,他使用的是[coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker)这个项目,所以我也给我部署的代码中加上了这个脚本,总算是解决了这个问题。
|
||||
部署好这个项目之后我试用了几下,虽然有些操作仍然会导致系统冻结,但相比Joel的版本来说已经好多了。很遗憾的是目前这个WASM Linux还不能和外界通信,所以作用不是很大,另外如果想在里面运行其他二进制程序还是相当困难,首先在WASM中不存在内存管理单元(MMU),不能实现隔离和分页的功能,另外以WASM作为指令集的环境下编译的产物也得是WASM,所以目前来说想用它做点什么还是不太合适。
|
||||
以上的这两个将Linux内核编译为WASM的方案其实相当于给内核打补丁,然后把浏览器看作是虚拟机来运行,有点像Xen,不过还有一种让Linux原生运行在WASM的[项目](https://github.com/okuoku/wasmlinux-project),它将[Linux kernel library](https://github.com/lkl/linux)编译为了WASM。那么什么是LKL?简单来说它有点像Wine,就和我之前所说的[OS模拟器](/2024/12/08/simulator.html)差不多,可以提供一个环境,让程序以为自己在Linux下运行,所以说它和之前的实现有一些不一样,它不存在内核模式,更像是一个普通的程序,而不是系统了。
|
||||
不过这个项目的体验也比较一般,它无论做什么都得按两次回车,看说明的意思貌似是因为没有实现异步信号传递,所以要手动打断`read`函数,而且也经常莫名其妙卡住,总体体验不如Thomas的项目。
|
||||
## 模仿的Linux
|
||||
其实如果只是想做到和Linux类似的功能,也有这样的项目,比如[WebContainers](https://github.com/stackblitz/webcontainer-core),它没有运行Linux系统,但是模拟了一个环境,可以在浏览器中运行Node.js以及Python之类的脚本,而且让脚本以为自己在Linux中运行,除此之外它还能用Service Worker把环境中运行的端口映射给浏览器,可以算是真的把服务端跑在浏览器上了。这个技术还挺高级,不过想想也挺合理,毕竟有WASI,直接编译为WASM的程序也不需要操作系统就能运行,所以用WASM去运行Linux本来就有点多此一举了😂。不过很遗憾的是WebContainers也不是开源软件,要使用它只能引入StackBlitz的资源,而且全网完全没有开源的替代品……也许在浏览器上进行开发本来就是个伪需求,所以没什么人实现吧。
|
||||
当然如果只是实现和WebContainers类似的功能,[JupyterLite](https://github.com/jupyterlite/jupyterlite)也可以实现,它可以在浏览器中像使用本地JupyterLab那样运行JS和Python,还能用Matplotlib、Numpy、Pandas进行数据处理,功能可以说非常强大,而且还是开源软件。只不过它没有模拟操作系统的环境,所以不能运行Node.js项目,也不能提供终端,所以不太符合我想要的效果……
|
||||
|
||||
# 总结
|
||||
总的来说,如果想要在博客上搞Linux终端,目前来看似乎虚拟机方案会更靠谱一些,虽然相对来说效率可能比较低,但毕竟目前WASM方案的可靠性还是不够,而且考虑到还需要配置额外的响应头,感觉有点麻烦,当然我觉得WASM还是算未来可期的,如果成熟的话肯定还是比虚拟机要更好一些,毕竟没有转译性能肯定要好不少。至于WebContainers这种方案……等什么时候有开源替代再考虑吧,需要依赖其他服务感觉不够可靠。只是也许我的想法只需要模拟一个合适的文件系统,然后给WASM版的Busybox加个终端就够了?不过这样感觉Bug会更多😂。
|
||||
至于打算什么时候给博客加上这个功能?应该也是未来可期吧😝,目前还没什么好的思路,仅仅是分享一下在浏览器中运行Linux的各种方法。
|
||||
20
_posts/2026-01-01-summary.md
Normal file
20
_posts/2026-01-01-summary.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
layout: post
|
||||
title: 年终总结
|
||||
tags: [总结]
|
||||
---
|
||||
|
||||
0 error(s), ∞ warning(s)<!--more-->
|
||||
|
||||
# 2025年的状态
|
||||
在2025年,感觉状态不如去年……由于没能做出正确的选择,还是有点糟糕。不过总的来说还没有引发关键性的错误,至少还能继续坚持下去。
|
||||
在这一年中,感觉记忆和思考能力都有所下滑,看来是没把自己照顾好😂,不过看看这一年写的文章,看起来似乎比以前更流畅了,这也许是因为和AI聊得多了,以至于思维有点偏向AI了吧。
|
||||
总的来说感觉自己的稳定性还是有点低了,但这可能不是我能独自解决的,也不知会有什么转机……
|
||||
|
||||
# 2025年发生的事情
|
||||
回顾了一下[去年的年终总结](/2025/01/01/summary.html),发现自己还是没能做到知行合一,在这一年里全球各类资产突然开始大幅升值,也就是说钱真的开始不值钱了……那时候想着买黄金,这一年下来却没能下定决心,最终错过了资产保值的机会。至于现在,似乎什么也做不了了……当然这对我的生活并没有造成什么严重的打击,只是感受到环境对自己的影响罢了。
|
||||
至于AI……依然是一天比一天强,而各个公司对AI的投入相比去年也是极大的提升,当然出来的效果也是非常强,那时候的AI还是挺容易出错,但是现在AI解决问题的能力已经可以替代很多人了,不只是文本生成模型,今年的图像与视频生成模型也真的是发展到了以往完全不能想象的地步,真的可以做到一句话想要什么就有什么了。
|
||||
另外,今年写的博客内容过于围绕博客本身了,以至于似乎不太跟得上时代,虽然我的博客也确实有点老旧了😆。只是看看以前的文章,都还有一些面向未来的趋势,而今年就有点“考古”了。相比于考古,去展望未来显然是更有意义的事情,只不过……真的感觉脑子不太好使,未来会发生什么,已经完全无法预测了。
|
||||
|
||||
# 展望2026年
|
||||
虽然不知道未来会发生什么,但毕竟还没有造成关键性的错误,还有修正的余地,只能希望未来能够做出正确的选择,不要让自己陷入危险的境地吧。
|
||||
34
_posts/2026-02-08-xslt.md
Normal file
34
_posts/2026-02-08-xslt.md
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
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',
|
||||
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',
|
||||
}
|
||||
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/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: true,
|
||||
]
|
||||
|
||||
const stream = await env.AI.run('@cf/google/gemma-3-12b-it', {
|
||||
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'));
|
||||
} 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/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();
|
||||
}
|
||||
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 {
|
||||
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
|
||||
});
|
||||
}
|
||||
} 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(
|
||||
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 (result != data) {
|
||||
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL, is_vec = 0 WHERE id = ?2")
|
||||
.bind(data, query).run();
|
||||
}
|
||||
if (!count) {
|
||||
count = 0;
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
resp = resp.map(respObj => {
|
||||
respObj.id = encodeURI(respObj.id);
|
||||
return respObj;
|
||||
});
|
||||
}
|
||||
return Response.json(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
} 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 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
|
||||
});
|
||||
} else {
|
||||
return Response.redirect("https://mabbs.github.io", 302)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ git --work-tree=/home/mayx/blog --git-dir=/home/mayx/blog.git checkout -f
|
||||
cd blog
|
||||
mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle2.7 exec jekyll build -d ../public_html
|
||||
bundle exec jekyll build -d ../public_html
|
||||
tar czvf MayxBlog.tgz --exclude-vcs ../public_html/
|
||||
mv MayxBlog.tgz ../public_html/
|
||||
cd ../public_html/
|
||||
|
||||
131
assets/css/feed.css
Normal file
131
assets/css/feed.css
Normal file
@@ -0,0 +1,131 @@
|
||||
@namespace atom "http://www.w3.org/2005/Atom";
|
||||
@namespace content "http://purl.org/rss/1.0/modules/content/";
|
||||
@namespace dc "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
body,
|
||||
rss,
|
||||
atom|feed {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto,
|
||||
"Noto Sans SC", "PingFang SC",
|
||||
"Microsoft YaHei", Arial, sans-serif;
|
||||
background: #f4f5f7;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 2em 1em;
|
||||
/* 左右内边距 1em,竖屏不贴边 */
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
max-width: 780px;
|
||||
/* 最大宽度,桌面端居中 */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
channel>title,
|
||||
atom|feed>atom|title {
|
||||
display: block;
|
||||
font-size: 1.7em;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2em 0;
|
||||
text-align: center;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
item,
|
||||
atom|entry {
|
||||
display: block;
|
||||
background: #ffffff;
|
||||
padding: 1.1em 1.2em;
|
||||
margin-bottom: 1.1em;
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.04),
|
||||
0 2px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
item>title,
|
||||
atom|entry>atom|title {
|
||||
display: block;
|
||||
font-size: 1.15em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.45em;
|
||||
color: #111;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
item>description,
|
||||
atom|entry>atom|summary {
|
||||
display: block;
|
||||
color: #555;
|
||||
line-height: 1.65;
|
||||
max-height: 16.5em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
item>description::after,
|
||||
atom|entry>atom|summary::after {
|
||||
content: "…";
|
||||
position: absolute;
|
||||
right: 0.3em;
|
||||
bottom: 0;
|
||||
padding-left: 1.5em;
|
||||
background: linear-gradient(to right,
|
||||
rgba(255, 255, 255, 0),
|
||||
#ffffff 70%);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
link,
|
||||
guid,
|
||||
pubDate,
|
||||
author,
|
||||
category,
|
||||
comments,
|
||||
source,
|
||||
enclosure,
|
||||
content|encoded,
|
||||
dc|creator,
|
||||
lastBuildDate,
|
||||
atom|id,
|
||||
atom|link,
|
||||
atom|updated,
|
||||
atom|published,
|
||||
atom|author,
|
||||
atom|category,
|
||||
atom|rights,
|
||||
atom|content,
|
||||
language,
|
||||
generator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
channel>description,
|
||||
atom|feed>atom|subtitle {
|
||||
display: block;
|
||||
margin: 0.4em 0 2em 0;
|
||||
/* 与条目明显拉开 */
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
channel>title,
|
||||
atom|feed>atom|title {
|
||||
margin-bottom: 0.4em;
|
||||
/* 原来较紧,这里放松 */
|
||||
}
|
||||
|
||||
atom|feed>atom|subtitle::after,
|
||||
channel>description::after {
|
||||
content: "这是一个订阅源(Feed)。复制当前URL到任何支持 Atom/RSS 的阅读器,即可订阅本博客的最新文章。\A以下展示了此订阅源包含的最新文章:";
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 0.95em;
|
||||
color: #666;
|
||||
margin: 1em 0 2em 0;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -34,33 +34,33 @@ a:hover {
|
||||
|
||||
.post-content h1 {
|
||||
text-indent: -8px;
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.post-content h2 {
|
||||
text-indent: -6px;
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
text-indent: -5px;
|
||||
}
|
||||
|
||||
.post-content h4 {
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
text-indent: -4px;
|
||||
}
|
||||
|
||||
.post-content h5 {
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
text-indent: -3px;
|
||||
}
|
||||
|
||||
.post-content h6 {
|
||||
margin:20px 0 10px;
|
||||
margin: 20px 0 10px;
|
||||
text-indent: -2px;
|
||||
}
|
||||
|
||||
@@ -121,42 +121,48 @@ div.highlight button:hover {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.footnotes p {
|
||||
margin: 0;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
.wrapper {
|
||||
width: 90%;
|
||||
}
|
||||
header{
|
||||
|
||||
header {
|
||||
width: 25%;
|
||||
}
|
||||
footer{
|
||||
|
||||
footer {
|
||||
width: 25%;
|
||||
}
|
||||
section{
|
||||
|
||||
section {
|
||||
width: 65%;
|
||||
}
|
||||
@media print, screen and (max-width: 960px) {
|
||||
|
||||
@media print,
|
||||
screen and (max-width: 960px) {
|
||||
.wrapper {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
header {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
section {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
code.highlighter-rouge{
|
||||
code.highlighter-rouge {
|
||||
padding: .1em .2em;
|
||||
margin: 0;
|
||||
font-size: 90%;
|
||||
@@ -171,9 +177,17 @@ code.highlighter-rouge{
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 300px;
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
td.h-entry {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td.h-entry:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
35
assets/css/xslt.css
Normal file
35
assets/css/xslt.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@namespace xsl "http://www.w3.org/1999/XSL/Transform";
|
||||
|
||||
xsl|template {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 2em 1em;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
box-sizing: border-box;
|
||||
margin-left: max(1em, env(safe-area-inset-left));
|
||||
margin-right: max(1em, env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
:root::before {
|
||||
content: "💀 这个 XSLT 模板已被谷歌 (Chrome) 杀死";
|
||||
display: block;
|
||||
color: #d93025;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
padding: 20px;
|
||||
border: 2px solid #d93025;
|
||||
border-radius: 8px;
|
||||
background: #fff1f0;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 12px rgba(217, 48, 37, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
1
rss.xml
1
rss.xml
@@ -3,6 +3,7 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type="text/xml" href="/feed.xslt.xml"?>
|
||||
<?xml-stylesheet type="text/css" href="/assets/css/feed.css"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>{{ site.title | xml_escape }}</title>
|
||||
|
||||
Reference in New Issue
Block a user