blog / atom.xml
User Mayx
update
547c86c
<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xml" href="/feed.xslt.xml"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="/atom.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2025-10-25T23:10:15+08:00</updated><id>/atom.xml</id><title type="html">Mayx的博客</title><subtitle>Mayx&apos;s Home Page</subtitle><author><name>mayx</name></author><entry><title type="html">一次找回GitHub上被删除仓库的经历</title><link href="/2025/10/12/recover.html" rel="alternate" type="text/html" title="一次找回GitHub上被删除仓库的经历" /><published>2025-10-12T00:00:00+08:00</published><updated>2025-10-12T00:00:00+08:00</updated><id>/2025/10/12/recover</id><content type="html" xml:base="/2025/10/12/recover.html">&lt;p&gt;在GitHub中寻找踪迹也许是非常简单的事情……&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前段时间,有人和我聊天的时候提到了&lt;a href=&quot;https://esolangs.org/wiki/Brainfuck&quot;&gt;Brainfuck&lt;/a&gt;语言,让我回想起了高中时写的&lt;a href=&quot;/%E6%BC%94%E8%AE%B2%E7%A8%BF/2018/06/20/Coding.html&quot;&gt;演讲稿&lt;/a&gt;。那时候我在演讲时也介绍了Brainfuck语言。对于Brainfuck的解释器,&lt;a href=&quot;https://rosettacode.org/wiki/RCBF&quot;&gt;各种语言都可以实现&lt;/a&gt;,不过我当时为了方便理解用了一个在GitHub Pages上的网站,用可视化的方式演示了它的运行过程,效果很不错。现在既然聊到了,自然就想分享一下这个&lt;a href=&quot;https://fatiherikli.github.io/brainfuck-visualizer/&quot;&gt;演示的网站&lt;/a&gt;,但我正想打开时,发现网站已经404了😰。 &lt;br /&gt;
在GitHub Pages上的网站都有对应的仓库,现在不仅原仓库消失了,连作者的&lt;a href=&quot;https://github.com/fatiherikli&quot;&gt;首页&lt;/a&gt;都打不开,看样子是完全退出GitHub了……那么我想找到这个网站的想法就无法实现了吗?不过GitHub有些有意思的特性也许能帮助我找回这个网站。&lt;/p&gt;
&lt;h1 id=&quot;github的特性&quot;&gt;GitHub的特性&lt;/h1&gt;
&lt;p&gt;在GitHub中,一个普通的仓库可能没有什么特别的,也许就是服务器上的一个文件夹。但是当仓库被其他人Fork的时候就不一样了,在执行Fork时,显然GitHub不会完整复制整个仓库。否则,同一个仓库在服务器上会占用双倍空间,这显然不合理。另外,想想Git的结构:它由提交对象和分支指针构成,每次提交都有唯一的Hash值且不会冲突。因此可以推测,GitHub在实现Fork时,所有被Fork的仓库可能共享同一个对象库,而每个用户仓库只保存指针,这样所有仓库只会占用增量空间,而不会存储重复内容。 &lt;br /&gt;
但这样也会带来一个问题,首先因为很多人可能要共用一部分对象,所以也很难确认对象的所有权,而且也因为这个原因所有的对象要能被所有人访问。因此在整个Fork网络中,只要有一个仓库存在,GitHub就必须保留所有的对象,而且每个仓库都能访问这个网络中所有的对象。为了验证这一点,我们可以用最知名的&lt;a href=&quot;https://github.com/torvalds/linux&quot;&gt;Linux内核仓库&lt;/a&gt;做个示例。 &lt;br /&gt;
首先对Linux仓库进行Fork,然后我们可以随便做一些改动,比如在README中写“Linux已经被我占领了😆”之类的内容,提交到自己的仓库,并且记下提交的Hash值,接下来就可以把自己的仓库删掉了。如果上面的猜想是正确的,那么在这个Fork网络中的任何一个仓库查看我刚刚的提交应该都可以,于是我直接在主仓库拼上了&lt;a href=&quot;https://github.com/torvalds/linux/tree/78e1d0446b94012da8639aa2b157d4f2dee481ce&quot;&gt;提交的Hash值&lt;/a&gt;(顺便一说只要值唯一,和其他的提交不冲突,&lt;a href=&quot;https://github.com/torvalds/linux/tree/78e1d044&quot;&gt;短的Hash值&lt;/a&gt;也可以),果不其然能找到刚刚修改的内容,这样一来,只要GitHub和任意一个Linux仓库的Fork还存在,这个提交就永远存在了😝。&lt;/p&gt;
&lt;h1 id=&quot;找回仓库&quot;&gt;找回仓库&lt;/h1&gt;
&lt;p&gt;那么接下来找回之前网站的方案就很简单了,我只要找到网站仓库的任意一个Fork,然后只要知道最新的提交Hash,我就可以还原最新的仓库了。Fork倒是好找,随便搜一下&lt;a href=&quot;https://github.com/ashupk/brainfuck-visualizer&quot;&gt;就能找到一个&lt;/a&gt;。这个Fork的最新提交是2016年,但要想找到我当年演讲的版本至少到2018年之后。不过这个Hash值也不太好找,虽然理论上爆破短Hash值也可以,但是感觉太麻烦了,没有那个必要,所以我干脆直接去互联网档案馆看看能找到的&lt;a href=&quot;https://web.archive.org/web/20201229125043/https://github.com/fatiherikli/brainfuck-visualizer/&quot;&gt;最新的仓库页面&lt;/a&gt;吧,这样我就能找到它的Hash值了,然后我再把Fork仓库的地址和Hash拼到一起,就看得到最新代码了。 &lt;br /&gt;
当然,仅仅看到代码还不够。我想Fork这个项目并在自己的GitHub Pages上部署一份。有没有什么好办法可以将我仓库的HEAD指针指向最新的提交呢?其实很简单,首先我要Fork这个Fork仓库,然后Clone我的仓库到本地。不过,此时Clone下来的仓库并不包含GitHub上完整的对象库,因此直接checkout或reset是不行的。这时Hash值就派上用场了,通过fetch拉取对应提交后,就可以进行上述操作。具体命令如下:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git fetch origin &amp;lt;commit-hash&amp;gt;
git reset &lt;span class=&quot;nt&quot;&gt;--hard&lt;/span&gt; &amp;lt;commit-hash&amp;gt;
git push origin master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最终我就获得了包含&lt;a href=&quot;https://github.com/Mabbs/brainfuck-visualizer&quot;&gt;最新代码&lt;/a&gt;&lt;a href=&quot;https://mabbs.github.io/brainfuck-visualizer/&quot;&gt;Brainfuck可视化演示&lt;/a&gt;了🎉。&lt;/p&gt;
&lt;h1 id=&quot;结局&quot;&gt;结局&lt;/h1&gt;
&lt;p&gt;后来我才知道,原来有一个专门的组织&lt;a href=&quot;https://archive.softwareheritage.org&quot;&gt;Software Heritage&lt;/a&gt;会保存所有代码,根本没必要搞这些花里胡哨的操作😂,像这个仓库也是能很轻易在&lt;a href=&quot;https://archive.softwareheritage.org/browse/origin/directory/?origin_url=https://github.com/fatiherikli/brainfuck-visualizer&quot;&gt;上面&lt;/a&gt;找到,这下以后知道了,再遇到类似情况就可以直接去Software Heritage查找,而不必在互联网档案馆上找线索瞎折腾了🤣。&lt;/p&gt;</content><author><name>mayx</name></author><category term="GitHub" /><category term="Git" /><category term="代码恢复" /><category term="软件存档" /><summary type="html">在GitHub中寻找踪迹也许是非常简单的事情……</summary></entry><entry><title type="html">关于ZIP Quine与自产生程序的探索</title><link href="/2025/09/01/quine.html" rel="alternate" type="text/html" title="关于ZIP Quine与自产生程序的探索" /><published>2025-09-01T00:00:00+08:00</published><updated>2025-09-01T00:00:00+08:00</updated><id>/2025/09/01/quine</id><content type="html" xml:base="/2025/09/01/quine.html">&lt;p&gt;描述自己的代码……是一种什么样的感觉?&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前段时间我在折腾&lt;a href=&quot;/2025/08/10/tilde.html#%E4%BD%BF%E7%94%A8git-hooks%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%8D%9A%E5%AE%A2&quot;&gt;博客部署&lt;/a&gt;的时候,回顾起了好久以前写的&lt;a href=&quot;/deploy.sh&quot;&gt;部署脚本&lt;/a&gt;。对于全站打包的这个步骤,本来我打算利用这个压缩包结合&lt;a href=&quot;/2025/08/01/sw-proxy.html&quot;&gt;Service Worker做离线浏览&lt;/a&gt;,但因为没有合适的方案所以放弃了。而现在对于这个压缩包,我又有了一个特别的想法。事实上在这个下载全站的压缩包中,里面的内容和实际的网站并不完全相同,因为在这个压缩包里缺少了压缩包本身。所以把这个压缩包解压之后直接当作网站打开,会发现下载压缩包的链接是无效的,除非在解压之后把压缩包移动到网站里才行…… &lt;br /&gt;
于是我就在想有没有一种可能可以让压缩包解压之后里面又包含了这个压缩包本身?似乎是个不太可能的事情,但我以前听过类似的东西,也许并非不可能?所以这次就来探索一下吧。&lt;/p&gt;
&lt;h1 id=&quot;自包含压缩包的探索&quot;&gt;自包含压缩包的探索&lt;/h1&gt;
&lt;p&gt;在很久之前,我见到过一个很知名的自包含压缩包(又称为ZIP Quine),叫做&lt;a href=&quot;https://alf.nu/s/droste.zip&quot;&gt;droste.zip&lt;/a&gt;,是由Erling Ellingsen&lt;a href=&quot;https://web.archive.org/web/20090106171423/http://tykje.com/code/useless/zip-file-quine&quot;&gt;在2005年制作&lt;/a&gt;出来的。当时我只知道它很神奇,原理什么的并不清楚,另外在网上也基本上找不到类似的压缩包。现在再回看时发现&lt;a href=&quot;https://alf.nu/ZipQuine&quot;&gt;介绍&lt;/a&gt;里包含了一些相关的链接,甚至还有一篇能自己制作类似压缩包的论文,所以接下来就可以看一下这些链接来理解这种压缩包是如何制作的了。 &lt;br /&gt;
关于原理方面,先看&lt;a href=&quot;https://github.com/wgreenberg&quot;&gt;Will Greenberg&lt;/a&gt;制作的一个&lt;a href=&quot;https://wgreenberg.github.io/quine.zip/&quot;&gt;示例&lt;/a&gt;,在这里面有一个谜题,使用“print M”(原样输出接下来的M行输入内容)和“repeat M N”(从倒数第N行的输出内容开始,重复M行)这两个指令让最终执行的结果和输入的指令完全相同。这正是对DEFLATE压缩算法所使用的LZ77编码的一种简化模拟,也就是说只要解决了这个问题,就可以让压缩包在解压时原样输出自己了。 &lt;br /&gt;
这个问题看起来还挺复杂,不过在仓库的&lt;a href=&quot;https://github.com/wgreenberg/quine.zip/issues/1&quot;&gt;Issues&lt;/a&gt;就有人给出了几种解法(当然,这个题目解法不唯一),所以在理论上应该是可行的,那么接下来就需要研究压缩文件的格式来实现它了。&lt;/p&gt;
&lt;h2 id=&quot;实现zip-quine的探索&quot;&gt;实现ZIP Quine的探索&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://swtch.com/~rsc/&quot;&gt;Russ Cox&lt;/a&gt;写的《&lt;a href=&quot;https://research.swtch.com/zip&quot;&gt;Zip Files All The Way Down&lt;/a&gt;》文章中,同样说明了这个原理,而且给出了一个方案,让上述这两个命令除了能够对命令本身的重复以外,还可以添加一些额外数据,这样才能做到构建一个压缩包文件。按照文章的描述,如果用之前谜题的规则来说,我们设头和尾的内容都是“print 0”,那么Cox给出的方案如下:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;print 0
print 2
print 0
print 2
repeat 2 2
print 1
repeat 2 2
print 1
print 1
print 4
repeat 2 2
print 1
print 1
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 0
print 0
print 2
repeat 4 4
print 0
print 0
print 2
repeat 2 2
print 0
repeat 2 2
print 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们把这些指令粘贴到&lt;a href=&quot;https://wgreenberg.github.io/quine.zip/&quot;&gt;quine.zip&lt;/a&gt;这个谜题中,就会发现输出和输入完全相同,以此就能验证Cox方案的正确性。除此之外作者还给出了生成的源代码:&lt;a href=&quot;http://swtch.com/rgzip.go&quot;&gt;rgzip.go&lt;/a&gt;,只是代码里面到处都是用来构建压缩包的十六进制数字,完全看不懂😂。 &lt;br /&gt;
另外这个方案是针对使用基于LZ77与哈夫曼编码的DEFLATE压缩算法,所以格式不重要。因此无论是ZIP,还是GZIP,以及TGZ(GZIP压缩后的TAR),其实都是一样的,因为他们都使用的是DEFLATE压缩算法。顺便一提,&lt;a href=&quot;https://github.com/honno&quot;&gt;Matthew Barber&lt;/a&gt;写了一篇很棒的&lt;a href=&quot;https://github.com/honno/gzip-quine&quot;&gt;文章&lt;/a&gt;,通过动画演示并详细讲解了如何实现一个简单的GZIP版ZIP Quine,很值得一看。 &lt;br /&gt;
还有一点,普通的TAR文件能否实现类似功能呢?从原理来说估计不行,因为TAR文件本身并没有压缩,也不包含指令,就单纯是一堆文件和元数据的拼接,所以就做不到自包含了。 &lt;br /&gt;
这么来看既然TGZ可以,那是不是在我博客网站的压缩包里放一份和自己一模一样的压缩包是可行的?很遗憾按照这个方法来看是做不到的,由于压缩格式和编码的限制,这个方案在实际实现时发现操作码需要是5个字节,最后发现最多只有类似&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repeat 64 64&lt;/code&gt;这样的指令能够满足要求,因此头尾区最多只能放64-5=59个字节的数据,也就刚刚好能容纳压缩格式需要的内容,几乎没法塞更多东西进去……显然,这些限制导致这种方式对我来说意义就不大了,何况作者的代码我也看不懂……而且还要考虑压缩包还存在校验用的CRC32,需要找满足整个压缩包的CRC32正好在压缩包中的“不动点”。虽然从CRC32的原理来说应该有办法做到通过数学方式解决,但这篇文章的作者因为解决了自包含的问题之后累了,因此放弃继续研究,选择直接暴力破解,毕竟CRC32只有32位,估计思考的时间都要比爆破的时间长吧😂。但如果是这样,即使有方案能存下我博客的数据,也不能在每次网站构建的时候都制作一次了…… &lt;br /&gt;
虽然Russ Cox写的文章看起来做不到包含更多内容了,但Erling Ellingsen制作的droste.zip却包含了一张图片,说明并不是没办法加入更多数据,只是没有找到正确的方法。在2024年&lt;a href=&quot;https://github.com/ruvmello&quot;&gt;Ruben Van Mello&lt;/a&gt;写了一篇论文《&lt;a href=&quot;https://www.mdpi.com/2076-3417/14/21/9797&quot;&gt;A Generator for Recursive Zip Files&lt;/a&gt;》,在这篇论文里他不仅解决了包含的额外数据过少的问题,还编写了一个通用工具,能让普通人也能生成这样的压缩包,而且他还创新性的做了一种像衔尾蛇一样的双层嵌套循环压缩包,非常的有意思,所以接下来我打算试试他的方案。 &lt;br /&gt;
在这篇论文中,里面简述了之前Russ Cox写的内容,也提到了59字节的限制,于是作者对原有的结构进行了一些改动,让操作码可以超出5字节的限制,具体可以看论文的表6,从而解决了只能包含59字节额外数据的限制。但由于DEFLATE压缩格式本身的约束(16位存储块长度以及32KiB回溯窗口),即使能够添加文件,最多也只能额外容纳32763字节的数据(其中包括压缩包所需的文件头)……显然这点空间完全存不下我的博客😭,看来我只能打消这个想法了。但既然都研究了半天,也不一定要存我的博客嘛,可以看看还有没有别的东西可以存?在这之前先继续阅读论文,看完再说吧。&lt;/p&gt;
&lt;h2 id=&quot;制作一个嵌套循环的zip-quine&quot;&gt;制作一个嵌套循环的ZIP Quine&lt;/h2&gt;
&lt;p&gt;在实现了常规的ZIP Quine之后,接下来就是作者的创新点了(如果光是解决存储限制这点创新点估计还不够发论文吧😂)。作者接下来制作了一种循环压缩文件,在压缩包内包含文件A和压缩包A,而压缩包A中则包含文件B和最初的压缩包,从而形成一个循环递归的结构。看论文的描述所说如果把外层的压缩包和内层的压缩包的开头和结尾按照一定的规则交替混合,就可以看作是一个整体,然后按照之前做ZIP Quine那样处理就可以……具体实现的细节得看论文的表10。只不过既然是把两个压缩包看作一个整体的话,按照上面的限制,自然每个压缩包能容纳的数据量就更小了,每个最多只能容纳16376字节的数据…… &lt;br /&gt;
另外既然这里面有两个压缩包,那么每个压缩包还有自己的CRC32校验和,理论上如果要爆破的话计算难度得是原来的平方,这样难度就太大了。不过作者发现如果把数据的CRC32值取反(即与“0xFFFFFFFF”取异或)然后和原始数据拼到一起,整个数据的CRC32校验和就会被重置为一个固定的值“0xFFFFFFFF”,看起来挺有意思,正常的哈希算法可没有这种特性。因此原本计算难度很大的爆破计算现在就可以和之前一样了…… &lt;del&gt;话说为什么不让两层的CRC32都这样计算(包括之前单层的ZIP Quine)?这样就不需要爆破了……貌似是因为在普通的ZIP Quine中满足条件的CRC32需要出现两次,所以不能用这个方案吧?&lt;/del&gt; &lt;br /&gt;
现在所有的理论都足够了,我需要挑一个文件来做这样嵌套循环的ZIP Quine,既然博客的大小不可以……要不然我就用我写过的第一个大项目——&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;吧,这个项目的主程序是22KiB,看起来似乎超出了嵌套循环ZIP Quine的限制?其实没有,它的限制指的是压缩后的大小,我这个程序压缩之后是8KiB左右,所以完全没问题。 &lt;br /&gt;
接下来就该使用论文中提到的生成工具:&lt;a href=&quot;https://github.com/ruvmello/zip-quine-generator&quot;&gt;zip-quine-generator&lt;/a&gt;,这是一个Kotlin编写的程序,从发布中可以下载预构建的程序,接下来只要按照README中的描述使用“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--loop&lt;/code&gt;”参数就可以用这个程序创建嵌套循环的ZIP Quine了。不过它原本的代码不能修改里面生成的压缩包的名字,另外&lt;a href=&quot;https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L845&quot;&gt;压缩后的文件属性是隐藏文件&lt;/a&gt;,还有&lt;a href=&quot;https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L29&quot;&gt;生成的压缩包中文件的创建时间总是当前时间&lt;/a&gt;,以及&lt;a href=&quot;https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L30&quot;&gt;给文件内填充额外数据的代码里面填的是作者的声明&lt;/a&gt;,表示文件是由他论文的所写的生成器生成的……这些情况让我感觉有点不爽,还是希望这些部分能自定义一下,所以我就小改了一下他的代码。顺便一说,Kotlin编译起来还挺简单,直接一句&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kotlinc src/main/kotlin -include-runtime -d output.jar&lt;/code&gt;就可以了,也不需要折腾Maven之类乱七八糟的东西。最终我修改并编译完程序之后就把文件丢到服务器上开始给我爆破CRC32了,花了10个小时就算出来了,倒是比想象中快😂。 &lt;br /&gt;
(2025.09.26更新)在2025年9月15日的时候,&lt;a href=&quot;https://github.com/NateChoe1&quot;&gt;Nate Choe&lt;/a&gt;给zip-quine-generator做了个&lt;a href=&quot;https://github.com/ruvmello/zip-quine-generator/pull/3&quot;&gt;重大贡献&lt;/a&gt;,他通过&lt;a href=&quot;https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm&quot;&gt;数学的方式&lt;/a&gt;让CRC32的值可以不需要通过爆破的方式算出来,现在想要再制作这样的压缩包就可以瞬间生成了……要是我再晚点做这个压缩包就不需要花那么长时间了吧🤣。 &lt;br /&gt;
最终我给我的&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;项目创建了&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project/releases/tag/Final-version&quot;&gt;Infinite Mabbs&lt;/a&gt;这个发布,生成的文件也可以在&lt;a href=&quot;/assets/Mabbs.zip&quot;&gt;这里&lt;/a&gt;下载,这也算是不枉我研究半天这个论文了😆。&lt;/p&gt;
&lt;h1 id=&quot;自产生程序的探索&quot;&gt;自产生程序的探索&lt;/h1&gt;
&lt;p&gt;说起来自包含压缩包为什么叫做ZIP Quine?其中的Quine是什么意思呢?其实这是一位美国哲学家的名字,他提出了“自指”的理论概念,所以为了纪念他,有类似概念的东西就被称作Quine,具体为什么也可以去看&lt;a href=&quot;https://en.wikipedia.org/wiki/Quine_(computing)#Name&quot;&gt;维基百科&lt;/a&gt;的说明。现在提到Quine一般代表的就是自产生程序,而自包含压缩包因为实现的原理和自产生程序的原理差不多,所以叫做ZIP Quine。因此接下来我打算探索一下自产生程序,更深入地了解Quine。&lt;/p&gt;
&lt;h2 id=&quot;实现quine的探索&quot;&gt;实现Quine的探索&lt;/h2&gt;
&lt;p&gt;那么什么是自产生程序?简单来说就是程序的源代码和程序的输出完全相同的程序,而且通常来说不允许通过读取/输入源代码的方式实现。按照一般的想法,让程序输出自身就需要输出中有全部代码,整个代码就会变长,而更长的代码就要输出更多,然后代码就会越来越长……所以这么想来似乎成了个死胡同。但其实这种程序实现起来并不复杂,想想ZIP Quine的实现,关键在于指令还需要以数据的形式表现,并且能被引用,这样输出的时候就会连着指令一起输出了。比如用Python的Quine举例:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;c = %r; print(c %% c)&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这里的变量中就以数据的形式存储了程序的代码,而在输出的时候除了变量内的代码,又通过引用的方式又把变量的内容放回到赋值的地方,所以它的输出就和原本的代码一样了。 &lt;br /&gt;
其实Quine的实现思路都差不多是这样,可以在&lt;a href=&quot;https://rosettacode.org/&quot;&gt;Rosetta Code&lt;/a&gt;中找到&lt;a href=&quot;https://rosettacode.org/wiki/Quine&quot;&gt;各种语言实现的Quine&lt;/a&gt;,在这其中能够发现大多数高级语言的写法都是类似的,除了一些低级语言以及esolang……这些我也看不懂😂,主要是有些语言没有变量的概念,不知道是怎么区分代码和数据……除了那个网站,在&lt;a href=&quot;https://esolangs.org/wiki/List_of_quines&quot;&gt;这里&lt;/a&gt;还能找到更多由esolang编写的Quine,可以看出来基本上很难看懂,其中最令人望而生畏的还得是&lt;a href=&quot;https://lutter.cc/malbolge/quine.html&quot;&gt;用Malbolge写的Quine&lt;/a&gt;,这个代码看起来不仅很长,而且像乱码一样。至于什么是Malbolge?这就是Malbolge程序:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;D&apos;&amp;lt;;_98=6Z43Wxx/.R?Pa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;代码就像加了密似的,顺便一说这个执行的输出结果是“Mayx”,关于Malbolge的具体细节可以看它的&lt;a href=&quot;http://www.lscheffer.com/malbolge_spec.html&quot;&gt;规范&lt;/a&gt;,另外虽然这个语言写起来很复杂,但还是有人能用它编出程序的,甚至还有人用&lt;a href=&quot;https://esolangs.org/wiki/Malbolge_Unshackled&quot;&gt;Malbolge Unshackled&lt;/a&gt;(Malbolge不限内存的变种)写过&lt;a href=&quot;https://github.com/iczelia/malbolge-lisp&quot;&gt;Lisp解释器&lt;/a&gt;,实在是恐怖如斯😨。&lt;/p&gt;
&lt;h2 id=&quot;只能quine的语言&quot;&gt;只能Quine的语言&lt;/h2&gt;
&lt;p&gt;其实想要做出Quine,还有一种更加无聊的方案,那就是设计一种只能Quine的语言🤣。根据Quine的定义,代码输出的结果就是它本身……所以我们可以把任何内容都看作代码,然后这种语言的行为就是输出所有代码……听起来是不是有点无聊?但是想想看如果把Linux中的cat命令当作解释器,就可以实现这种语言了,比如:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#!/bin/cat
Hello, world!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;作为脚本执行的结果就是原样输出这段内容,不过把内容当作代码算不算作弊呢……如果看作是cat的输入显然是作弊,但如果是当作源代码的话应该就不算了吧😋……但这就不是能写出逻辑的语言了。所以说Quine的趣味并不在“能不能实现”,而在于如何在限制条件下实现。正是因为大多数语言不会直接“自我输出”,才会觉得那些精巧的Quine程序如此有意思。&lt;/p&gt;
&lt;h2 id=&quot;quine-relay的探索&quot;&gt;Quine Relay的探索&lt;/h2&gt;
&lt;p&gt;还有一个更加复杂的Quine变种是“Quine接力”(Quine Relay),即一个程序输出另一个程序的源代码,另一个程序又输出下一个程序的源代码,最后回到原始程序,就和之前所说的嵌套循环ZIP Quine有点类似。最著名的例子是&lt;a href=&quot;https://github.com/mame&quot;&gt;Yusuke Endoh&lt;/a&gt;(这位还是&lt;a href=&quot;https://www.ioccc.org/&quot;&gt;IOCCC&lt;/a&gt;的冠军之一)创建的&lt;a href=&quot;https://github.com/mame/quine-relay&quot;&gt;quine-relay&lt;/a&gt;项目,它包含了128种编程语言的循环。 &lt;br /&gt;
这种程序写起来会更复杂一些,不过原理都差不多,通常除了当前运行的部分是可执行代码外,其他的代码都需要以额外包含的数据形式(如字符串)存储在变量中。如果想自己做个类似简单的Quine Relay,除了去看&lt;a href=&quot;https://en.wikipedia.org/wiki/Quine_(computing)#Ouroboros_programs&quot;&gt;维基百科&lt;/a&gt;之外,前段时间我还看到过一个不错的&lt;a href=&quot;https://blog.mistivia.com/posts/2024-09-21-quine/&quot;&gt;文章&lt;/a&gt;,里面就讲了如何用“笨办法”编写Quine和Quine Relay,通过把变量中的内容编码为16进制来避免不同语言可能存在的特殊字符转译问题,思路不错,对于理解如何编写这类程序的问题很有帮助。当然这只是个&lt;strong&gt;简单&lt;/strong&gt;的方案,仅适用于一些常规的编程语言,像上面那个&lt;a href=&quot;https://github.com/mame/quine-relay&quot;&gt;quine-relay&lt;/a&gt;项目中甚至还包含Brainfuck之类的esolang,这种估计得要想办法让相对高级一些的语言通过“生成”的方式得到输出下一种代码的代码,而不是简单的赋值了,所以只靠这点知识想去完全理解大佬的作品还是想多了😆。 &lt;br /&gt;
顺便一说,quine-relay并不是那位大佬唯一的Quine作品,他还做过&lt;a href=&quot;https://github.com/mame/radiation-hardened-quine&quot;&gt;有冗余的Quine&lt;/a&gt;以及&lt;a href=&quot;https://mamememo.blogspot.com/2010/09/qlobe.html&quot;&gt;动态的Quine&lt;/a&gt;,真的是相当的厉害……&lt;/p&gt;
&lt;h2 id=&quot;polyglot-quine的探索&quot;&gt;Polyglot Quine的探索&lt;/h2&gt;
&lt;p&gt;除了Quine Relay之外还有一种很复杂的Quine,叫做&lt;a href=&quot;https://en.wikipedia.org/wiki/Polyglot_(computing)&quot;&gt;Polyglot&lt;/a&gt; Quine,与Quine Relay需要在程序执行后才能切换到其他语言接力不同,Polyglot Quine的源代码本身即可同时属于多种语言,而且用这些语言的解释器每个执行后的输出全都一样,都与源代码完全一致。由于不同的编程语言的格式既有些相同之处,也有很多不同之处,所以让同一份代码表示不同语言就会很容易产生歧义,这时候就只能想办法通过一些特别的方式(比如将可能会对当前语言产生干扰的代码看作是注释的方式)来规避语言之间的差异。 &lt;br /&gt;
Quine本身就已经很困难了,再加上这些限制就变得更加复杂了,所以制作Polyglot Quine的编程语言基本上都得精挑细选,而且通常只有两种语言,比如&lt;a href=&quot;https://github.com/TrAyZeN/polyglot-quine/blob/master/main.c&quot;&gt;这段代码&lt;/a&gt;就是C和Python的Polyglot Quine,它巧妙利用了C预处理器指令在Python中可视为注释的特性,使两种语言互不干扰,非常有趣。当然并不是说只能是两种语言,像&lt;a href=&quot;https://github.com/2KAbhishek/polyquine&quot;&gt;这个&lt;/a&gt;项目甚至使用了五种语言(C、Perl、PHP、Python、Ruby),可以说是相当厉害了。除此之外更令人惊叹的则是&lt;a href=&quot;https://github.com/d0sboots/PyZipQuine&quot;&gt;PyZipQuine&lt;/a&gt;项目,在这其中LZ77编码也可以作为一种语言,所以既可以被当作压缩包,也可以作为Python2.7代码,而且二者都是Quine,实在是令人赞叹。&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;虽然这次探索最终没能完成让包含博客所有内容的压缩包自包含,但是在探索的过程中我还是收获了不少,尤其是Ruben Van Mello制作的ZIP Quine生成工具,实在是太棒了。很久以前我见到droste.zip这个压缩包的时候,就想整一个属于自己的ZIP Quine,现在我不仅用那个生成工具做了一个,还是对我来说很有意义的第一个项目——Mabbs,而且更关键的还是生成的是比普通的ZIP Quine更高级的嵌套循环ZIP Quine,也算是圆了小时候的心愿了。 &lt;br /&gt;
另外在探索自产生程序的时候,也发现了一些很有意思的网站,比如&lt;a href=&quot;https://rosettacode.org/&quot;&gt;Rosetta Code&lt;/a&gt;以及&lt;a href=&quot;https://esolangs.org/&quot;&gt;Esolang wiki&lt;/a&gt; &lt;del&gt;(虽然这个网站里被好多小学生写了一堆无聊的东西😂)&lt;/del&gt; ,里面有不少有趣的东西,也算是让我大开眼界了。 &lt;br /&gt;
所以有的时候探索不一定要完成目标,在这个过程中也会收获到很多不错的东西吧😊。&lt;/p&gt;</content><author><name>mayx</name></author><category term="压缩包" /><category term="Quine" /><category term="自产生程序" /><category term="Quine Relay" /><summary type="html">描述自己的代码……是一种什么样的感觉?</summary></entry><entry><title type="html">在Tilde社区的游玩体验</title><link href="/2025/08/10/tilde.html" rel="alternate" type="text/html" title="在Tilde社区的游玩体验" /><published>2025-08-10T00:00:00+08:00</published><updated>2025-08-10T00:00:00+08:00</updated><id>/2025/08/10/tilde</id><content type="html" xml:base="/2025/08/10/tilde.html">&lt;p&gt;Tilde社区,如“家”一般的感受😝&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;/2025/08/01/sw-proxy.html&quot;&gt;上一篇文章&lt;/a&gt;里,我说到给我的博客增加了不少网站&lt;a href=&quot;/proxylist.html&quot;&gt;镜像&lt;/a&gt;,也在这个过程中发现了不少Git平台实例。顺便一提,我找到了个不错的&lt;a href=&quot;https://github.com/ecosyste-ms/repos&quot;&gt;仓库&lt;/a&gt;,可以全网搜索各种Git平台实例。在这探索的过程中,我发现了一种神奇的社区——Tilde社区,体验之后感觉非常有意思,所以来分享一下。&lt;/p&gt;
&lt;h1 id=&quot;什么是tilde社区&quot;&gt;什么是Tilde社区&lt;/h1&gt;
&lt;p&gt;Tilde社区之所以叫Tilde,是因为在类Unix系统(如Linux、BSD)中,波浪号(Tilde)“~”代表家目录。因此,Tilde社区就是基于类Unix系统环境,并且可以公共登录的服务器,又被称为&lt;abbr title=&quot;public access unix systems&quot;&gt;pubnixes&lt;/abbr&gt;。一般这些社区的管理员会预装很多软件、开发环境以及一些公共服务,比如聊天室、邮件、BBS论坛等,这些构成了社区互动的基础。不过并不是所有类似这样提供Shell访问的公共服务器都可以被称作社区,比如知名的免费网站托管商&lt;a href=&quot;https://www.serv00.com&quot;&gt;Serv00&lt;/a&gt;虽然也提供可以登录的FreeBSD服务器,并且在服务器上安装了非常多的工具和环境,从表面来看和Tilde社区提供的服务几乎一模一样,但是它少了一个很重要的东西,那就是社区,它的权限管理非常严格,不允许服务器的用户互相串门,也没有互相交流的平台,而且它的本质是商业服务(尽管是免费的),所以它不算Tilde社区。 &lt;br /&gt;
至于Tilde社区的加入方式,一般可以通过填写在线申请表、私信或发送邮件申请,有些比较有特色的社区会用SSH交互等方式。审核通过后,管理员就会在服务器上为你创建账户,即可获得属于自己的“家”,一般的Tilde社区在这个过程中不需要付一分钱,因为他们通常都是反商业化的,如果遇到了需要付钱才能激活账户的公共服务器,那就不是Tilde社区,即使它历史悠久,可能是别的什么东西😆。 &lt;br /&gt;
那么在哪里可以找到它们呢?有一个不错的网站,叫做&lt;a href=&quot;https://tildeverse.org&quot;&gt;tildeverse&lt;/a&gt;,这不仅是一个Tilde社区的集合,它自身也提供了很多服务。不过总的来说各个社区之间也是互相独立的,tildeverse只是提供了一个平台让大家可以互相沟通,所以这个网站叫做“loose association”,就相当于博客中的博客圈一样。 &lt;br /&gt;
于是我在tildeverse的成员列表中随便挑选了几个Tilde社区提交了注册申请,过了一段时间申请通过了,那么接下来就来说说我在Tilde社区的体验吧。&lt;/p&gt;
&lt;h1 id=&quot;tilde社区的体验&quot;&gt;Tilde社区的体验&lt;/h1&gt;
&lt;p&gt;虽然我加入了不少Tilde社区,不过各个社区提供的服务都差不多,首先最重要的就是个人主页,一般Tilde社区基本上都会提供一个像&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/public_html&lt;/code&gt;这样的目录存放个人主页的网页文件,并且可以通过类似&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com/~username&lt;/code&gt;这样的地址访问,还有些社区会允许通过二级域名的方式访问,类似&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username.example.com&lt;/code&gt;这样,像我博客好多地方写的都是从根路径开始,就很适合用二级域名的方式。这些主页大多也支持使用PHP之类的网页,不过不像虚拟主机那样有个面板可以轻松安装扩展和切换版本,有些可能要自己写配置文件,有些可能要管理员才可以操作,毕竟是社区,所以不太注重用户体验。 &lt;br /&gt;
当然除了HTTP协议的个人主页,通常他们还可以创建一些Gemini协议和Gopher协议的个人主页,这些协议不支持普通浏览器访问,需要用&lt;a href=&quot;https://github.com/rkd77/elinks&quot;&gt;ELinks&lt;/a&gt;之类的文本浏览器才能打开,这个浏览器甚至可以在终端里用鼠标操作😆。不过因为协议非常简单,所以内容也就只能整些文本内容了。 &lt;br /&gt;
除了个人主页外,一般还会提供编写博客的程序,比如&lt;a href=&quot;https://github.com/cfenollosa/bashblog&quot;&gt;bashblog&lt;/a&gt;,用这个编写好之后就可以直接生成HTML网站,能直接发布到自己的主页上让别人访问。这个脚本还是纯Bash的,就和我当年的&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;一样,看起来还挺酷,当然功能上肯定比不上正经的静态博客生成器😆。 &lt;br /&gt;
当然博客是一方面,还可以写微博,他们一般提供一款叫&lt;a href=&quot;https://github.com/buckket/twtxt&quot;&gt;twtxt&lt;/a&gt;的软件,用这个软件可以使用命令发微博,还能关注其他人,查看时间线,而且这还是去中心化的,可以跨服务器进行关注,感觉就和&lt;a href=&quot;https://github.com/mastodon/mastodon&quot;&gt;Mastodon&lt;/a&gt;一样。 &lt;br /&gt;
除此之外作为社区当然就会有聊天室和论坛了,不过这些聊天室和BBS论坛通常不会像大多数人使用的那种通过Web或者图形界面来查看,而是纯文本的那种,比如论坛通常会用&lt;a href=&quot;https://github.com/bbj-dev/bbj&quot;&gt;Bulletin Butter &amp;amp; Jelly&lt;/a&gt;,聊天室会用IRC,可以使用&lt;a href=&quot;https://github.com/weechat/weechat&quot;&gt;WeeChat&lt;/a&gt;,只是我对IRC的印象不太好,在终端使用的IRC客户端没有一个使用体验好的😅,相比于其他在终端使用的软件,操作通常只需要一些快捷键,而且界面上通常会有提示,而IRC客户端就只能敲命令,而且还担心敲错了当成普通内容发出去……所以尽管我加入了Tilde社区,受限于聊天软件的使用体验以及我的英文水平,所以并不能和在服务器上的其他人聊天,没法参与到社区中,这么来看似乎我只能把Tilde社区当作普通的共享服务器来看待了😭。 &lt;br /&gt;
在Tilde社区中既然都是用类Unix系统,自然大都是会写程序的人,所以托管代码也很重要,不过因为大多Tilde社区的主机性能很垃圾,所以很多都不会提供Git平台服务,即使有可能也只会提供Gitea,像GitLab这种对服务器要求比较高的基本上就不会有了。但很多人可能对Git有误解,其实绝大多数情况下都不需要Git平台来托管代码,之所以用Gitea、GitLab的工具是因为它们有比较完整的用户管理以及代码协作能力,比如Issue和Wiki之类的,但是大多数人其实根本没必要用到这些功能,有问题发邮件就好了,像Linux的开发就完全没有用Gitea、GitLab之类的平台。所以在Tilde社区中托管代码非常简单,直接新建个文件夹,执行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git init --bare&lt;/code&gt;,那就是个仓库,另外很多Tilde社区提供&lt;a href=&quot;https://git.zx2c4.com/cgit/about/&quot;&gt;cgit&lt;/a&gt;方便让公众在网页上查看和克隆自己的仓库,一般只要放到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/public_git&lt;/code&gt;目录下就可以。至于自己如果想要提交代码,可以用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git remote add tilde ssh://example.com/~/public_git/repo.git&lt;/code&gt;添加远程仓库,本地改完之后push上去就可以。 &lt;br /&gt;
不过用那些Git平台还有一个地方可能会用到,那就是CI/CD,直接用命令创建的仓库它可以做到CI/CD吗?其实是可以的,Git有hooks功能,如果想要类似CI/CD的功能就可以直接用post-receive这个钩子,提交完成之后就会执行这个脚本,所以接下来就讲讲我是如何用Git hooks在服务器上自动部署我的博客吧。&lt;/p&gt;
&lt;h1 id=&quot;使用git-hooks自动部署博客&quot;&gt;使用Git hooks自动部署博客&lt;/h1&gt;
&lt;p&gt;我的博客使用的是&lt;a href=&quot;https://github.com/jekyll/jekyll&quot;&gt;Jekyll&lt;/a&gt;框架,这是一个使用Ruby编写的静态博客生成器。所以要想构建我的博客至少要有Ruby的环境,还好几乎所有的Tilde社区都预装了,不用担心环境的问题。 &lt;br /&gt;
不过Tilde社区一般不提供root权限,所以Ruby的包需要放到自己的目录下,比如可以执行这样的命令:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bundle2.7 config &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--local&lt;/span&gt; path &lt;span class=&quot;s1&quot;&gt;&apos;/home/mayx/blog-env&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;然后再在我的仓库下执行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle2.7 install&lt;/code&gt;就可以了。 &lt;br /&gt;
接下来就需要编写构建的脚本,这个倒是简单,直接用我的&lt;a href=&quot;/deploy.sh&quot;&gt;部署脚本&lt;/a&gt;改改就行:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /home/mayx/
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; public_html
git &lt;span class=&quot;nt&quot;&gt;--work-tree&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/home/mayx/blog &lt;span class=&quot;nt&quot;&gt;--git-dir&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/home/mayx/blog.git checkout &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;blog
&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;Mabbs
curl &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
bundle2.7 &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;jekyll build &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; ../public_html
&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;czvf MayxBlog.tgz &lt;span class=&quot;nt&quot;&gt;--exclude-vcs&lt;/span&gt; ../public_html/
&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;MayxBlog.tgz ../public_html/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;写完之后把这个脚本放到仓库的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hooks/post-receive&lt;/code&gt;下,然后加上执行权限就可以用了,以后每次push之后都会直接更新我在Tilde社区的主页,也就是我的镜像站。这样部署不像一般CI/CD还要额外装环境,直接使用提前装好的环境,构建速度会快不少。 &lt;br /&gt;
不过既然有机会构建了,我就可以把一些不支持构建的Pages用起来了,有些Forgejo实例支持Pages功能,但是仓库里只能包含构建后的代码,还有Bitbucket Cloud也是一样的问题,所以我可以把构建后的文件夹转为仓库,然后推送到这些Git平台上。 &lt;br /&gt;
考虑到我的网站每次构建基本上所有的页面都有改动,因此我不打算保留提交记录,所以我每次都会重新初始化git仓库,不过在我实际测试的时候,发现钩子触发的脚本执行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git init&lt;/code&gt;的时候创建的是裸仓库……查了一下貌似是环境变量的问题,只要把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GIT_DIR&lt;/code&gt;变量删掉就没问题了,以下是实际的代码:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ../public_html/
&lt;span class=&quot;nb&quot;&gt;unset &lt;/span&gt;GIT_DIR
git init
git add &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;update&quot;&lt;/span&gt;
git remote add codeberg ssh://git@codeberg.org/mayx/pages.git
git remote add gitgay ssh://git@git.gay/mayx/pages.git
git remote add bitbucket ssh://git@bitbucket.org/unmayx/unmayx.bitbucket.io.git
git push &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; codeberg master
git push &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; gitgay master
git push &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; bitbucket master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;除了这些Pages之外,还有一些平台只支持使用他们自己的软件上传网站代码,比如surge,既然我可以在构建的时候执行命令,那就顺带一起上传吧,比如我可以这样执行:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/home/mayx/blog-env/node_modules/surge/bin/surge /home/mayx/public_html/ mayx.surge.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;其实除了这个之外我还想上传到sourcehut pages,这个也需要用他们自己的软件上传,但是sourcehut pages的CSP太严格了,居然禁止脚本访问其他网站😭,这样我的文章点击计数、文章推荐、AI摘要之类乱七八糟的功能就全用不了了,所以只好作罢……&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;总的来说,这次在Tilde社区的各种体验还挺有意思,虽然没能和各个社区的成员进行对话,但是在探索的过程中,也了解到了不少新知识,而且也给我的博客增加了不少镜像。不知道会不会有哪个社区成员在闲逛的时候看到我的博客然后对里面的内容感兴趣😝……要是有哪个成员看到然后给我评论,那也算是社区互动吧😋。虽然我的文章内容都是中文,但现在翻译软件也足够强大了,应该不至于拦住外国人。只是在国内似乎没有见过类似的社区,在国内也有的话,那就可以用中文和大家对话了吧。&lt;/p&gt;</content><author><name>mayx</name></author><category term="tilde" /><category term="服务器" /><category term="git" /><category term="体验" /><summary type="html">Tilde社区,如“家”一般的感受😝</summary></entry><entry><title type="html">用Service Worker实现一个反向代理</title><link href="/2025/08/01/sw-proxy.html" rel="alternate" type="text/html" title="用Service Worker实现一个反向代理" /><published>2025-08-01T00:00:00+08:00</published><updated>2025-08-01T00:00:00+08:00</updated><id>/2025/08/01/sw-proxy</id><content type="html" xml:base="/2025/08/01/sw-proxy.html">&lt;p&gt;现代浏览器真是强大,可以替代一些服务器的功能了!&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前段时间在和群友聊天的时候,提到了我博客的&lt;a href=&quot;/2022/02/14/move.html&quot;&gt;分发方案&lt;/a&gt;,这么多年过去之后我已经在很多平台上&lt;a href=&quot;/proxylist.html&quot;&gt;分发&lt;/a&gt;了我的博客,不过这只是多重冗余,并不算去中心化(虽然我也有向IPFS同步,不过IPFS还得pin,也不太可靠)……所以这么看来,我的博客似乎还不算极其可靠😂?但其实不完全是这样。因为除了向不同平台的分发,我的博客还有一个全文搜索的功能。更重要的是,之前做&lt;a href=&quot;/2024/10/01/suggest.html&quot;&gt;文章推荐功能&lt;/a&gt;时,会把整个博客所有文章的文字存到访客浏览器的localStorage中。这么说来,只要有人访问了我博客的文章,他们的浏览器中就会保存一份我博客文章的完整文本副本。从这个角度看,可靠性应该算是相当高了吧? &lt;br /&gt;
不过我之前的分发方案里还记录了一点,在GitHub Pages以外的平台我还打包了一份全站生成后的代码,之所以要全站打包,也是希望我的博客能尽可能的分发,考虑到几乎所有的Linux发行版一定有tar,而不一定有zip,所以我最终打包成了tgz格式。如果能让访客下载这个全站打包好的副本,相比于浏览器里只存储了文章文字的全文数据,这应该是一个更好的备份方式吧?毕竟我的博客本身也是我的作品……所以这个压缩包到底有什么地方可以用到呢? &lt;br /&gt;
这时候我想起来,现代的浏览器功能已经非常强大了,甚至在浏览器里直接运行一个Web服务器也完全没问题。如果能让访客在浏览器里下载那个压缩包并运行一个Web服务器,那就相当于在他们本地设备上部署了一份我的博客副本。这样一来,除了我自己搭建的网站之外,这些访客的本地也运行着一个我的博客实例😆(当然,这份副本只有访客自己能看到)。&lt;/p&gt;
&lt;h1 id=&quot;研究实现方案&quot;&gt;研究实现方案&lt;/h1&gt;
&lt;p&gt;想要在浏览器上运行Web服务器其实很简单,那就是使用Service Worker,它可以完全离线在浏览器上工作。格式的话和以前写过的Cloudflare Worker非常相似,毕竟Cloudflare Worker就是模仿Service Worker的方式运行啊😂,所以我要是想写Service Worker应该很简单。 &lt;br /&gt;
有了执行的东西之后就是存储,在Service Worker上存储可以用Cache Storage,用它的话不仅可以保存文件的内容,还可以保存响应头之类的东西,用来和Service Worker配合使用非常的方便,不过既然是Cache,它的可靠性就不能保证了,浏览器很可能在需要的时候清除缓存内容,所以相比之下用IndexedDB应该会更可靠一些。 &lt;br /&gt;
那么接下来就该处理我的tgz文件了,tgz的本质是tar文件被gzip压缩之后的东西。浏览器解压gzip倒是简单,可以用Compression Stream API,但它也只能处理gzip了……对于tar的处理似乎就必须用第三方库。而tar的库在网上搜了搜似乎很少,网上找了个&lt;a href=&quot;https://github.com/gera2ld/tarjs&quot;&gt;tarjs&lt;/a&gt;库,文档写的也看不懂,⭐️也很少,看来是有这个需求的人很少啊,而且还要用现代JS那种开发方式,要用什么npm之类的。在&lt;a href=&quot;/2025/07/24/screenshot.html&quot;&gt;上一篇文章&lt;/a&gt;我就说过我不是专门写前端的,对在自己电脑上安装Node.js之类的东西很反感。后来问AI也完全写不出能用的代码,估计这个功能还是太小众了……另外又想到除了这个问题之外还要处理网站更新的时候该怎么通知Service Worker之类乱七八糟的事情……所以只好作罢😅。&lt;/p&gt;
&lt;h1 id=&quot;使用service-worker进行反向代理&quot;&gt;使用Service Worker进行反向代理&lt;/h1&gt;
&lt;p&gt;这么看来离线运行我的博客似乎有点麻烦,不过既然都研究了一下Service Worker,不如想想其他能做的事情……比如当作反向代理?虽然在浏览器上搞反向代理好像意义不是很大……但值得一试。我之前见过一个项目叫做&lt;a href=&quot;https://github.com/EtherDream/jsproxy&quot;&gt;jsproxy&lt;/a&gt;,它是用Service Worker实现的正向代理,这给了我一些启发。我在之前研究分发方案的时候发现了一些模仿GeoCities的复古静态网站托管平台,比如&lt;a href=&quot;https://neocities.org&quot;&gt;Neocities&lt;/a&gt;&lt;a href=&quot;https://nekoweb.org&quot;&gt;Nekoweb&lt;/a&gt;。它们需要通过网页或API才能上传网站,不太方便使用CI/CD的方式部署。但是我又觉得它们的社区很有意思,所以想用Service Worker的方式反代到我的网站,显得我的网站是部署在它们上面一样。 &lt;br /&gt;
这个做起来非常简单,其实就和我以前用&lt;a href=&quot;/2021/03/02/workers.html#%E9%A6%96%E5%85%88%E7%BB%99%E8%87%AA%E5%B7%B1%E6%90%AD%E4%B8%AA%E5%8F%8D%E4%BB%A3&quot;&gt;Cloudflare Worker搭建反代&lt;/a&gt;几乎完全一样,遇到请求之后直接通过Fetch获取内容然后再返回就行,唯一不同的就是浏览器存在跨域策略,在跨域时只有对应网站存在合适的响应头才可以成功请求,还好我用的Pages服务大多都允许跨域。但是在我实际测试的时候发现这个允许跨域的等级不太一样,比如GitHub Pages的响应头里包含&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin: *&lt;/code&gt;,但是不允许OPTIONS方式请求,另外如果要修改请求头,在响应头里还要一一允许相应的请求头才行……当然对于这种问题解决起来很简单,就和我之前写的&lt;a href=&quot;/2025/04/08/feed.html&quot;&gt;订阅源预览&lt;/a&gt;一样,用&lt;a href=&quot;https://github.com/Zibri/cloudflare-cors-anywhere&quot;&gt;cloudflare-cors-anywhere&lt;/a&gt;搭建的CORS代理就可以,有了这个就可以轻松使用Service Worker反代其他网站了。 &lt;br /&gt;
当然对我来说其实有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin: *&lt;/code&gt;就够了,我也不需要花里胡哨的请求方式,也不需要在请求头和请求体里加什么莫名其妙的东西,所以对我来说直接请求我的某一个镜像站就可以,于是代码如下: &lt;br /&gt;
&lt;strong&gt;index.html&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Mayx的博客&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 注册 Service Worker&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;serviceWorker&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;serviceWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/sw.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Service Worker 注册成功:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;registration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 刷新网页&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Service Worker 注册失败:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://mabbs.github.io&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://mabbs.github.io&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Redirecting&lt;span class=&quot;ni&quot;&gt;&amp;amp;hellip;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://mabbs.github.io&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Click here if you are not redirected.&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;sw.js&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TARGET_SITE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;被反代的网站&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//也可以用CORS代理&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 强制立即激活新 Service Worker&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;skipWaiting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;activate&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 立即控制所有客户端&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;claim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;origin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;respondWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handleProxyRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handleProxyRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 构建目标 URL&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targetUrl&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;proxyUrl&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TARGET_SITE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targetUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pathname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targetUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 创建新请求(复制原请求属性)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;proxyRequest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;proxyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// headers: request.headers,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// body: request.body&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 发送代理请求&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;proxyRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 返回修改后的响应&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;statusText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;statusText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;headers&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Proxy error:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Proxy failed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最终的实际效果: &lt;a href=&quot;https://mayx.nekoweb.org&quot;&gt;https://mayx.nekoweb.org&lt;/a&gt;&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;虽然折腾了半天没能增强我博客的可靠性……但是体会到了现代浏览器的强大之处,难怪前几年会提出ChromeOS和PWA之类的东西,原来浏览器功能还是相当强大的,用了Service Worker以后即使是纯前端也可以有和使用服务器一样的体验,在过去的浏览器中要是想实现这样的功能……好像也不是不可能😂,用AJAX加服务器使用伪静态策略其实是可以做到的……其实Service Worker的功能更多还是在离线时使用的,我这个例子好像没体现它的优势😆。 &lt;br /&gt;
但总的来说相比以前想要实现这种反代的功能代码还是更清晰,也更简单了,也许以后如果有机会我又有心思让博客在访客浏览器上离线运行,那就可以体现Service Worker真正的优势了🤣。&lt;/p&gt;</content><author><name>mayx</name></author><category term="浏览器" /><category term="Service Worker" /><category term="Worker" /><category term="反向代理" /><summary type="html">现代浏览器真是强大,可以替代一些服务器的功能了!</summary></entry><entry><title type="html">使用Cloudflare制作自动更新的网站预览图</title><link href="/2025/07/24/screenshot.html" rel="alternate" type="text/html" title="使用Cloudflare制作自动更新的网站预览图" /><published>2025-07-24T00:00:00+08:00</published><updated>2025-07-24T00:00:00+08:00</updated><id>/2025/07/24/screenshot</id><content type="html" xml:base="/2025/07/24/screenshot.html">&lt;p&gt;Cloudflare的功能真是越来越多了,而且还免费!&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前段时间我在登录Cloudflare的时候发现Workers上多了一个“浏览器呈现”的功能(可能已经出来一段时间了,不过之前一直没关注),看介绍,这个功能可以让Worker操作运行在Cloudflare服务器上的浏览器。这功能挺有意思,而且免费用户也能用,不如想个办法好好利用一下。 &lt;br /&gt;
一般来说这个功能可以干什么呢?既然是在AI盛行的时候出现……估计是为了搞Agent之类的吧,不过看&lt;a href=&quot;https://developers.cloudflare.com/browser-rendering/platform/limits/&quot;&gt;文档&lt;/a&gt;对免费用户来说一天也只有10分钟的使用时间,估计也没什么应用价值……那除了这个之外还能做些什么?我发现有好多博客主题喜欢给自己的README里添加一个能查看主题在多种设备上显示效果的预览图,以展示主题的自适应能力。那么既然现在能在Cloudflare上操作浏览器,那么我也可以做一个类似的,而且这个预览图还可以自动更新。&lt;/p&gt;
&lt;h1 id=&quot;制作自适应的网站预览&quot;&gt;制作自适应的网站预览&lt;/h1&gt;
&lt;p&gt;既然打算做预览图,那么我应该用什么方案?按照不同尺寸的视口截几张图再拼起来吗?这显然就太复杂了,况且在Cloudflare Workers中处理图片也相当困难。这时我想起来曾经见到过一个工具,只要输入网址,就可以在一个页面中同时展示网站在四种不同设备(手机、平板、笔记本电脑、台式机)上的显示效果,叫做“多合一网页缩略图”,实现原理是使用iframe和CSS缩放模拟多种设备视口。搜了一下发现这套代码被不少网站使用,所以就随便找了其中一个工具站把代码和素材扒了下来,稍微改了一下,然后放到&lt;a href=&quot;https://github.com/Mabbs/responsive&quot;&gt;GitHub&lt;/a&gt;上,方便等一会用Cloudflare访问这个部署在&lt;a href=&quot;https://mabbs.github.io/responsive/&quot;&gt;GitHub Pages&lt;/a&gt;上的页面来进行截图。&lt;/p&gt;
&lt;h1 id=&quot;使用cloudflare浏览器呈现进行截图&quot;&gt;使用Cloudflare浏览器呈现进行截图&lt;/h1&gt;
&lt;p&gt;接下来截图就简单了,不过Cloudflare有两种截图的办法,&lt;a href=&quot;https://developers.cloudflare.com/browser-rendering/workers-bindings/&quot;&gt;用Workers&lt;/a&gt;的话可以直接用Puppeteer之类的库连接浏览器,但用这个库需要安装,要本地搭环境……我毕竟不是专门搞JS开发的,一点也不想在本地安装Node.js环境,所以就不想用这种方式。另外一种是通过&lt;a href=&quot;https://developers.cloudflare.com/browser-rendering/rest-api/&quot;&gt;调用Cloudflare的接口&lt;/a&gt;,这种非常简单,只需要填几个参数请求就行,唯一的问题就是要填一个Token……我一直觉得Worker调用Cloudflare自己的服务不应该需要Token之类的东西,毕竟内部就能验证了,没必要自己搞,但是我看了半天文档貌似无论如何只要想调接口就必须搞个Token……那没办法就搞吧,其实也很简单,只需要在“账户API令牌”里添加一个有浏览器呈现编辑权限的令牌就行。 &lt;br /&gt;
至于展示……这个接口调用比较耗时,而且一天只能调用10分钟,截图的话估计也就够30次左右,还有每分钟3次的限制😓,所以实时更新肯定是不行了,图片肯定得缓存,一天更新一次感觉应该就够了。另外次数这么少的话写成接口给大伙用貌似也没啥意义,所以我就把地址写死了,于是以下就是最终实现的代码:&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;caches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;kv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SCREENSHOT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://mabbs.github.io/responsive/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cacheKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;datedKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 工具函数:构建 Response 对象&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;content-type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;image/png&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;cache-control&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;public, max-age=86400, immutable&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 工具函数:尝试从 KV 和 Cache 中加载已有截图&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tryGetCachedResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;kvData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;kv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;arrayBuffer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;kvData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;kvData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 1. 优先使用当日缓存&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tryGetCachedResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 2. 若缓存不存在,则请求 Cloudflare Screenshot API&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;viewport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;800&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;gotoOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;networkidle0&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;apiRes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;`https://api.cloudflare.com/client/v4/accounts/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CF_ACCOUNT_ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/browser-rendering/screenshot?cacheTTL=86400`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`Bearer &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CF_API_TOKEN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apiRes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`API returned &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apiRes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;apiRes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arrayBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 后台缓存更新&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitUntil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;kv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;kv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;expirationTtl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;86400&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Screenshot generation failed:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// 3. 回退到通用旧缓存&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tryGetCachedResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Screenshot generation failed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;502&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;使用方法很简单,创建一个Worker,把以上代码粘进去,然后把从“账户API令牌”中生成的令牌填到Worker的密钥中,名称为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CF_API_TOKEN&lt;/code&gt;,另外再加一个名称为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CF_ACCOUNT_ID&lt;/code&gt;的密钥,内容是账户ID,就是打开仪表板时URL中的那串16进制数字,除此之外还需要创建一个KV数据库,绑定到这个Worker上,绑定的名称是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SCREENSHOT&lt;/code&gt;。如果想给自己的网站生成,可以Fork我的&lt;a href=&quot;https://github.com/Mabbs/responsive&quot;&gt;仓库&lt;/a&gt;,然后把里面首页文件中的网址替换成你的网站,然后再把Worker中的url替换成Fork后仓库的GitHub Pages地址就可以了。 &lt;br /&gt;
最终的效果如下: &lt;br /&gt;
&lt;img src=&quot;https://screenshot.mayx.eu.org&quot; alt=&quot;ScreenShot&quot; /&gt;&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;Cloudflare实在是太强了,虽然这个浏览器呈现免费用量并不多,但是有这么一个功能已经吊打很多Serverless服务了,毕竟浏览器对服务器资源的占用也不小,小内存的服务器甚至都不能运行,如果要自己搭的话成本可能也不小,而现在Cloudflare能免费提供,应该说不愧是赛博活佛吗🤣。&lt;/p&gt;</content><author><name>mayx</name></author><category term="Cloudflare" /><category term="Workers" /><category term="网站截图" /><category term="自动化" /><summary type="html">Cloudflare的功能真是越来越多了,而且还免费!</summary></entry><entry><title type="html">一次服务器被入侵的经历</title><link href="/2025/07/13/hacked.html" rel="alternate" type="text/html" title="一次服务器被入侵的经历" /><published>2025-07-13T00:00:00+08:00</published><updated>2025-07-13T00:00:00+08:00</updated><id>/2025/07/13/hacked</id><content type="html" xml:base="/2025/07/13/hacked.html">&lt;p&gt;即使是被入侵了也可以学到一些知识!&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前几天,我闲来无事登录了一下一台之前一直闲置的服务器,登录上去后,乍一看似乎没有任何问题,然后习惯性的执行了一下&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top&lt;/code&gt;命令看了一眼。从进程列表来看,似乎没有什么明显异常的地方,但是服务器的load值很高,cpu的us值也很高。 &lt;br /&gt;
以前我倒也遇到过几次load值很高的情况,一般是硬盘或NFS等网络存储挂了但是依然有程序在读写挂载的目录会有这种问题,但那种情况一般高的是cpu的wa值,而不是us值,us值是软件正常用掉的……但是进程列表里根本没有占CPU的程序啊……看来服务器是被入侵了😰。&lt;/p&gt;
&lt;h1 id=&quot;检查服务器&quot;&gt;检查服务器&lt;/h1&gt;
&lt;p&gt;虽然说是要查,但其实我根本不知道进程隐藏的原理😂,虽然听说过有恶意软件会这样做,现在遇到了一时半会又想不出来怎么找。还好这是台闲置的服务器,上面什么东西都没有跑,所以正常来说除了ssh连接之外,这个服务器不该有任何其他的连接,于是我执行了一下&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netstat -tanp&lt;/code&gt;看了一眼,发现有个奇怪的进程使用一个境外的IP和我的服务器建立了连接,用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ps -ef&lt;/code&gt;查了一下这个 PID,结果进程名显示为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[kcached]&lt;/code&gt;……这下给我整不会了。 &lt;br /&gt;
后来查了些资料知道了可以用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsof -p&lt;/code&gt;查看进程读取的文件,才看到木马的本体:&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/bin/gs-dbus&lt;/code&gt;。不过如果我只是杀掉这个进程然后删除文件,那攻击者肯定会重新回来,所以我得排除一下是不是还有别的木马文件。 &lt;br /&gt;
一般来说攻击者权限维持的方式大多是crontab,不过我看了一下配置文件里似乎没有,root下的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authorized_keys&lt;/code&gt;倒是有个陌生的公钥于是顺手删掉了……也没有其他文件夹下有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gs-dbus&lt;/code&gt;文件……难道没有别的木马文件了吗?后来我仔细找了一下,发现有个很可疑的文件&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/lib/libprocesshider.so&lt;/code&gt;,一看就不是什么好东西🤣,后来在GitHub上搜了一下,是&lt;a href=&quot;https://github.com/gianlucaborello/libprocesshider&quot;&gt;libprocesshider&lt;/a&gt;这个项目,就是它让我在top中什么也没找到的,看文档中应用是添加一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ld.so.preload&lt;/code&gt;文件,所以解除隐藏效果我也只需要删掉这个文件就好啦。 &lt;br /&gt;
不过感觉还是不够……所以我全盘搜索了一下&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libprocesshider.so&lt;/code&gt;文件,果不其然还有,通过那个文件在/usr/games里找到了木马的大本营,里面有一堆这个入侵者的工具,于是就顺手保存了一份然后从服务器上删掉了。 &lt;br /&gt;
另外还有自启动到底是怎么实现的?既然不是crontab……应该是systemd。看了一下果不其然有个服务在保持&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gs-dbus&lt;/code&gt;的运行,不过程序我已经删了,所以它现在只会不停尝试重启,接下来只需要停止并禁用这个服务就行了。 &lt;br /&gt;
至于为什么会被入侵……我也很清楚,其实并没有什么漏洞,单纯是设置的密码太简单了,被嘿客扫到啦!所以解决起来也很简单,把这些垃圾清除掉之后设置个稍微复杂一点的密码就行了。&lt;/p&gt;
&lt;h1 id=&quot;入侵分析&quot;&gt;入侵分析&lt;/h1&gt;
&lt;p&gt;既然这个嘿客都不删他的工具,留下来就是给我分析的吧?那么我就像&lt;a href=&quot;/2024/11/02/trojan.html&quot;&gt;上次&lt;/a&gt;一样分析一下他使用的工具吧~首先里面有个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy-all.sh&lt;/code&gt;文件,看起来应该是登录服务器之后最先执行的程序,在这里面有个压缩包,解压出来之后搜了一下里面的文件,发现是&lt;a href=&quot;https://github.com/hackerschoice/gsocket&quot;&gt;Global Socket&lt;/a&gt;项目,看起来应该是包含反弹Shell、伪装以及权限维持之类功能的一个小工具。看了下源代码才知道原来用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec -a&lt;/code&gt;就可以伪装进程的名称,而且那个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gs-dbus&lt;/code&gt;就是这个项目里的程序……这么看来挖矿的操作应该是入侵者远程执行的代码,所以在查找进程的时候发现了它吧。 &lt;br /&gt;
除此之外里面还有个logclean项目,看了一眼是&lt;a href=&quot;https://github.com/infinite-horizon219/mig-logcleaner-resurrected&quot;&gt;mig-logcleaner-resurrected&lt;/a&gt;项目,看起来应该是清除日志用的,不过我根本没从日志找它🤣,即使入侵者用了对我来说也没起到什么作用。不过倒也是个挺有用的项目,也许在某些扫尾工作很有用。 &lt;br /&gt;
最后就是&lt;a href=&quot;https://github.com/gianlucaborello/libprocesshider&quot;&gt;libprocesshider&lt;/a&gt;这个项目,也许还有其他隐藏进程的方式,不过知道这个项目之后最起码以后再遇到类似的情况我就会优先去看&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/ld.so.preload&lt;/code&gt;文件了。 &lt;br /&gt;
至于其他的就是一些爆破SSH的工具,估计是用来横向渗透的,看起来有点原始……也没啥用处,另外还有连接XMR矿池的一些配置文件,以及我也看不出来的玩意,应该就这么多有用的东西了。&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;虽然被入侵是没有预料的事情,但还好这个服务器是闲置的,装完系统之后上面什么有用的东西都没有,所以除了入侵者让它不太闲置赚了点小钱之外对我倒是没什么损失,另外还了解到了一些不错的小工具,这么看来入侵者赚的这点小钱就当是给他的学费吧🤣。&lt;/p&gt;</content><author><name>mayx</name></author><category term="Linux" /><category term="安全" /><category term="服务器" /><category term="入侵" /><summary type="html">即使是被入侵了也可以学到一些知识!</summary></entry><entry><title type="html">使用XSLT为博客XML文件编写主题一致的样式</title><link href="/2025/07/01/xslt.html" rel="alternate" type="text/html" title="使用XSLT为博客XML文件编写主题一致的样式" /><published>2025-07-01T00:00:00+08:00</published><updated>2025-07-01T00:00:00+08:00</updated><id>/2025/07/01/xslt</id><content type="html" xml:base="/2025/07/01/xslt.html">&lt;p&gt;虽然XML是机器读的内容……不过加上和主题一致的XSLT样式也算是一种细节吧~&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;/2025/06/02/optimize.html#%E5%AF%B9%E4%BA%8E%E8%AE%A2%E9%98%85%E8%BD%AF%E4%BB%B6%E7%9A%84%E5%85%BC%E5%AE%B9%E6%80%A7%E6%94%AF%E6%8C%81&quot;&gt;上一篇文章&lt;/a&gt;中,我提到在提高订阅源兼容性的时候给博客的订阅文件增加了一个XSLT样式。当时使用的样式是从&lt;a href=&quot;https://github.com/genmon/aboutfeeds/&quot;&gt;About Feeds&lt;/a&gt;下的一个&lt;a href=&quot;https://github.com/genmon/aboutfeeds/issues/26&quot;&gt;Issue&lt;/a&gt;中找的,里面有个基于&lt;a href=&quot;https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl&quot;&gt;Pretty Feed&lt;/a&gt;修改成能同时支持RSS和Atom格式的样式。虽然那个样式倒也说不上难看,但总觉得与我的博客整体风格有些割裂,所以这次打算制作一个和我博客主题完全一致的XSLT样式。&lt;/p&gt;
&lt;h1 id=&quot;制作订阅文件的xslt样式&quot;&gt;制作订阅文件的XSLT样式&lt;/h1&gt;
&lt;p&gt;虽然想搞这么一个样式,但是我用的Jekyll引擎不能在引用的布局外添加额外内容……如果我要自己写,要么把我的默认布局拆成头和尾两部分然后用include引用,要么把默认布局的代码直接复制一份到XSLT样式中。这两个方案我都不太满意,第一种我以后在修改默认布局时需要同时从两个文件检查上下文,很不方便;而第二种方案违反了DRY原则,也会增加以后修改的难度。所以要怎么办呢? &lt;br /&gt;
后来我想了想,如果不能通过直接引用默认布局在外面增加XSLT的代码,那干脆让默认布局引用一个XSLT布局吧!这样我就能在不复制默认布局也不进行过多修改的情况下在外面套XSLT的代码了。于是我就在最外面写了个符合XSLT格式的XML布局,让默认布局引用它。然后再写一个布局引用默认布局,让最外面的布局根据这个布局的名字来判断是否需要使用XSLT的布局,具体的实现可以看我的&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/tree/master/_layouts&quot;&gt;layout目录&lt;/a&gt;。另外有一些地方需要注意一下,作为XML,内容中不能包含未闭合的标签,所有自闭合标签结尾必须添加斜杠,属性必须有值,以及所有标签和属性大小写要一致……还好我平时修改布局文件以及编写内容的时候基本上都遵循了这些规则,所以没什么太多需要改动的地方。 &lt;br /&gt;
当时修改时,是模仿之前的那个样式进行的,原来那个样式在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;html&lt;/code&gt;元素上加了XML命名空间,但是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xsl:output&lt;/code&gt;配置的输出却是按照HTML的方式输出,结果导致内容中用于换行的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;br&lt;/code&gt;标签在实际转换中全部变成了两个标签……我猜应该是转换器看到XML命名空间后,先按照XHTML的规则把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;br&lt;/code&gt;解析成了一开一闭的一对标签,然后又根据HTML的转换规则把这对标签当作两个单独的标签输出了吧……但奇怪的是,只有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;br&lt;/code&gt;标签出现了这个问题,像&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hr&lt;/code&gt;等其他自闭合标签则没有……既然如此,只要把XML命名空间删掉就OK了。 &lt;br /&gt;
在改完之后虽然整体看上去和其他页面似乎已经很相似了,但总感觉还有些样式不太对劲……我猜应该是和文档类型声明有关系,我平时写的是HTML5,而XSLT默认转出来是HTML4.0……但是我不太清楚怎么解决这个问题,于是问了问AI,AI说在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xsl:output&lt;/code&gt;中加上&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctype-system=&quot;about:legacy-compat&quot;&lt;/code&gt;就行。最终改完试了下确实有效😂,样式上也没有出现奇怪的偏移了。 &lt;br /&gt;
最后把写好的布局应用到&lt;a href=&quot;/feed.xslt.xml&quot;&gt;/feed.xslt.xml&lt;/a&gt;中就可以了,之所以是这个路径是因为我用的&lt;a href=&quot;https://github.com/jekyll/jekyll-feed&quot;&gt;jekyll-feed&lt;/a&gt;只支持这个位置,至于我自己搞的RSS格式的订阅只需要在开头用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xml-stylesheet&lt;/code&gt;指令声明一下就行了。&lt;/p&gt;
&lt;h1 id=&quot;给xslt样式自己的样式&quot;&gt;给XSLT样式自己的样式&lt;/h1&gt;
&lt;p&gt;在写好给订阅文件用的XSLT样式之后,我发现XSLT样式本身也是个XML文件……既然我给订阅文件做了样式,那么也得给XSLT样式文件本身做个样式才对,但如果我单独写一个给它的样式,那岂不是要给样式的样式再写一个样式😂,所以肯定不能这样做。不过仔细想一下,还有个办法,可以让XSLT样式文件自引用自身的样式,这样就能避免之前担心的套娃问题了。所以接下来我应该在XSLT中写一个检测应用样式的XML文件是不是XSLT样式文件的代码,方法很简单,既然XSLT样式中肯定包含&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xsl:stylesheet&lt;/code&gt;这个元素,那么我可以判断如果存在这个元素,就可以确定这就是XSLT样式了,如果有人点开看了我就可以展示一个提示信息告诉访客这是一个样式文件,这样访客就不会看到那句“This XML file does not appear to have any style information associated with it. The document tree is shown below.”了😝。&lt;/p&gt;
&lt;h1 id=&quot;制作sitemap的xslt样式&quot;&gt;制作Sitemap的XSLT样式&lt;/h1&gt;
&lt;p&gt;既然给XSLT样式也加了样式……那我博客还有其他XML文件需要处理吗?似乎还有个Sitemap,我的Sitemap是&lt;a href=&quot;https://github.com/jekyll/jekyll-sitemap&quot;&gt;jekyll-sitemap&lt;/a&gt;插件生成的……那它支持加样式吗?虽然文档上没有写,不过看了眼源代码发现可以通过创建&lt;a href=&quot;/sitemap.xsl&quot;&gt;/sitemap.xsl&lt;/a&gt;文件添加,所以就顺手套用之前的样式搞了一个(虽然应该没有访客去看Sitemap😂,毕竟这是给搜索引擎用的)。可惜这些地址都是插件硬编码的,如果可以自己修改位置我就只写一个XSLT样式文件就可以了……&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;折腾了这么多整体展示效果还不错,虽然这些文件也许根本没人看😂(本来就不是给人读的),但也算展现了一下博客的细节之处吧,而且在折腾的时候至少还了解了不少关于XML和XSLT的知识(尽管在现代这些好像没啥用了)。当然重要的也许不是了解这些知识,而是这个过程吧……总的来说还是挺有意思的。&lt;/p&gt;</content><author><name>mayx</name></author><category term="XSLT" /><category term="博客优化" /><category term="XML" /><category term="Feed" /><summary type="html">虽然XML是机器读的内容……不过加上和主题一致的XSLT样式也算是一种细节吧~</summary></entry><entry><title type="html">近期对博客的修改与优化记录</title><link href="/2025/06/02/optimize.html" rel="alternate" type="text/html" title="近期对博客的修改与优化记录" /><published>2025-06-02T00:00:00+08:00</published><updated>2025-06-02T00:00:00+08:00</updated><id>/2025/06/02/optimize</id><content type="html" xml:base="/2025/06/02/optimize.html">&lt;p&gt;在修改博客的时候也能学到不少新知识啊~&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在两个月前,我写了一篇&lt;a href=&quot;/2025/04/04/search.html&quot;&gt;针对博客搜索功能优化的记录&lt;/a&gt;。在写完之后没几天,有位名叫&lt;a href=&quot;https://github.com/xymoryn&quot;&gt;@xymoryn&lt;/a&gt;的大佬看到了我的博客并且进行了&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/issues/167&quot;&gt;吐槽&lt;/a&gt;,内容很值得参考。不过我自从用&lt;a href=&quot;https://github.com/pages-themes/minimal&quot;&gt;minimal&lt;/a&gt;主题以来从来没有改过样式的原因主要还是写不来CSS😂,并不是真的不想改,但其中提到可以让AI优化,我觉得也很有道理,现在AI这么发达实在不会用AI改就好啦~&lt;/p&gt;
&lt;h1 id=&quot;对博客样式的优化&quot;&gt;对博客样式的优化&lt;/h1&gt;
&lt;p&gt;虽然大佬给出了参考的CSS,但我不太喜欢那种风格,尤其还把之前的左右布局改成了上下布局。我当年之所以选择minimal主题就是因为它是左右布局的,如果选择上下布局的话我还不如用&lt;a href=&quot;https://pages-themes.github.io/hacker/&quot;&gt;hacker&lt;/a&gt;这个主题,另外那个参考的CSS可能是因为AI写的,有很多没有考虑到的地方,比如主题自带的CSS鼠标放到链接上字体会变粗,然后可能会变宽,导致影响整体的布局,而参考的CSS选择直接让所有的链接放到上面都变细,即使原来是粗字体也变细,比如标题之类的,这就更难受了。像这种情况要怎么改呢?我还是希望能用minimal主题的CSS,但让链接变粗的体验确实不太好,所以我选择问问AI。 &lt;br /&gt;
最后AI给出的答复是使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;font-weight: inherit;&lt;/code&gt;,看起来确实解决了问题,不过如果鼠标移到链接上没有任何反应也不太好,所以就学GitHub在鼠标移到链接时加上了下划线。 &lt;br /&gt;
除此之外就是字号、行高和布局,字号和行高我也不希望改的太激进,所以就稍微加了一点点,看起来没那么密就好。至于布局,之前minimal主题的宽度是写死的,左边是270px,右边是500px,对于我的MacBook看起来也还好,因为MacBook的屏幕比较小,屏幕的利用率还是比较高的。不过对于更大的屏幕总共860px大小的区域确实不太够,尤其是4K屏幕可能只有中间一点点的区域有内容,会看着很难受,所以我想了一下还是改成百分比布局比较好,这样无论屏幕有多宽也能利用得到。 &lt;br /&gt;
还有一点就是分段,虽然我也知道在Markdown中两个换行是分段,但是感觉在文本中两个换行隔得太远了,所以一开始写文章的时候就选择只换行。不过在中文里确实不分段也不太好看,但是又不想去动之前写的文章,那该怎么办呢?思来想去干脆把换行全部替换成分段好啦,在Jekyll中可以用replace过滤器把所有的“&amp;lt;br&amp;gt;”替换成“&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;”,因为Markdown解析本来就会有一个段落,所以直接闭合加开始就能分割成多个段落了。那么加了分段是为了什么?其实主要是为了首行缩进,有首行缩进对阅读还是有挺大帮助的,至于怎么做也非常简单,直接给p标签设置&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-indent: 2em;&lt;/code&gt;就可以了。 &lt;br /&gt;
最后就是评论授权的问题,我用的Gitalk也有人问了&lt;a href=&quot;https://github.com/gitalk/gitalk/issues/95&quot;&gt;这个问题&lt;/a&gt;,我仔细看了一下GitHub官方文档中&lt;a href=&quot;https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps&quot;&gt;OAuth可以授权的作用域&lt;/a&gt;发现确实是没办法限制只写Issues😥,至于其他的评论系统对后端的依赖又太多了,尤其是Giscus,居然是直接用iframe引用Giscus网站中的页面😅,如果Giscus哪天挂了,那评论系统不也挂了(虽然GitHub也不可靠……),至于自托管就更不可能了,我能让服务器持续运营可比不上大厂😆。所以最后我选择给Gitalk加个提示,不想登录也可以跳转到GitHub上进行评论,至于怎么加?还是让AI来吧,最后AI给我写了这么一串CSS:&lt;/p&gt;
&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;.gt-btn-login&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::after&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;如果不想登录,请点击上方评论数跳转至对应ISSUE进行评论&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;absolute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;100%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;50%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-50%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#333&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;12px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;12px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;white-space&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nowrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opacity&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visibility&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;z-index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;.gt-btn-login&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:hover::after&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;visibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;visible&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;.gt-btn-login&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::after&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;margin-top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;.gt-btn-login&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::after&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;box-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2px&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0.15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;至此,关于博客样式的部分我觉得已经提高不少读者的用户体验了,也感谢大佬提出的建议。&lt;/p&gt;
&lt;h1 id=&quot;对博客兼容性的优化&quot;&gt;对博客兼容性的优化&lt;/h1&gt;
&lt;p&gt;最近由于某些原因我又用起Windows 7了。其实我觉得Windows 7是一个很不错的操作系统,有很多人性化的东西,比如桌面小工具,自带Feed订阅,还有&lt;a href=&quot;https://archive.org/details/wlsetup-all_201802&quot;&gt;Windows Live Essentials&lt;/a&gt;等等,可惜后来全部被微软砍掉了🤣。考虑到Windows 7如此优秀,那要不然兼容一下它旗下的Internet Explorer 8浏览器吧? &lt;br /&gt;
其实GitHub给的那些Jekyll主题本身都是兼容IE8的,包括我在用的minimal主题也一样。但随着我这么多年加了许许多多的功能,绝大多数功能都没有考虑兼容性,只想着能用就行。不过我写的功能基本上都非常简单,如果想改得让它兼容IE8也并非难事,只要理论上可行就可以。当然也有些理论上不可能的东西,比如WebGL。因此,我的&lt;a href=&quot;/Live2dHistoire/&quot;&gt;Live2D看板娘&lt;/a&gt;就没有任何可能性被支持了,至于其他的……也许有一些理论上可以支持,但是改起来比较麻烦的就也算了吧(比如Gitalk之类的)。&lt;/p&gt;
&lt;h2 id=&quot;对文章点击计数器的兼容性优化&quot;&gt;对文章点击计数器的兼容性优化&lt;/h2&gt;
&lt;p&gt;其实我的文章点击计数器从&lt;a href=&quot;/2019/06/22/counter.html&quot;&gt;之前&lt;/a&gt;改成用jQuery调用自己的接口以后就没有什么兼容性的问题了,因为jQuery本来就是处理浏览器之间差异的库,而且也是兼容IE8的。只不过有个问题是IE8不支持用XHR跨域请求,只能用“XDR(XDomainRequest)”进行跨域请求……还好有个&lt;a href=&quot;https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest&quot;&gt;现成的库&lt;/a&gt;能让jQuery在遇到这种情况时使用XDR请求,于是我就用条件注释让IE9以下的浏览器引入这个库,这样在IE下也能正常显示文章点击数了😆。&lt;/p&gt;
&lt;h2 id=&quot;关于响应式布局的兼容性优化&quot;&gt;关于响应式布局的兼容性优化&lt;/h2&gt;
&lt;p&gt;在IE8中的CSS是不支持媒体查询的,所以在修改窗口大小时也不能根据情况使用合适的样式。本来我没打算解决这个问题,结果恰好看到了一个库:&lt;a href=&quot;https://github.com/scottjehl/Respond&quot;&gt;Respond.js&lt;/a&gt;,所以就直接拿来用了😝。&lt;/p&gt;
&lt;h2 id=&quot;关于全文搜索的兼容性优化&quot;&gt;关于全文搜索的兼容性优化&lt;/h2&gt;
&lt;p&gt;其实从功能的角度来说这种东西肯定是在IE8下可以实现的,但是我用的&lt;a href=&quot;https://github.com/christian-fei/Simple-Jekyll-Search&quot;&gt;那个库&lt;/a&gt;有点迷,到处都用的是const关键字结果还莫名其妙&lt;a href=&quot;https://github.com/christian-fei/Simple-Jekyll-Search/blob/master/src/JSONLoader.js#L29&quot;&gt;判断XHR&lt;/a&gt;搞的好像是在兼容旧浏览器?改起来有点麻烦懒得搞了……不过除此之外还有个取巧的方式,既然我搜不了,干脆让谷歌来搜吧,至于谷歌支不支持IE8就不是我的事了🤣,所以直接给搜索框外面套了一个form表单,这样甚至可以在不启用JS的情况下搜索(假设谷歌支持没有JS的情况)。&lt;/p&gt;
&lt;h2 id=&quot;对于订阅软件的兼容性支持&quot;&gt;对于订阅软件的兼容性支持&lt;/h2&gt;
&lt;p&gt;之前我的博客对订阅的支持是使用的官方的&lt;a href=&quot;https://github.com/jekyll/jekyll-feed&quot;&gt;jekyll-feed&lt;/a&gt;插件,它只支持Atom格式的订阅,一般的阅读器也是支持这种格式的(即使是IE8也是完美支持)。但是我发现有非常少数的某些网站没办法解析Atom,只支持RSS……所以我只好特地加了&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/blob/master/rss.xml&quot;&gt;对RSS格式的支持&lt;/a&gt;,还顺带搞了支持Atom和RSS格式的&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/blob/master/feed.xslt.xml&quot;&gt;XSLT模板&lt;/a&gt;来预览。既然RSS也支持了,那干脆连&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/blob/master/feed.json&quot;&gt;JSONFeed&lt;/a&gt;也一起做了吧😆,虽然意义不是很大……&lt;/p&gt;
&lt;h2 id=&quot;给博客添加网页快讯&quot;&gt;给博客添加网页快讯&lt;/h2&gt;
&lt;p&gt;既然要兼容IE8,那当然是能用的都用啦,在IE8订阅网站源的地方,有一个‘添加网页快讯’的功能。因为没有可以参考的网站,我甚至都没理解这个功能展现的效果是什么样的。我看这个网页快讯好像是抄了一部分&lt;a href=&quot;http://microformats.org/wiki/hatom&quot;&gt;hAtom Microformat&lt;/a&gt;的规范,我还以为是每个条目都单独需要一个entry-title和entry-content,结果发现并不是😅,一个hslice只能有一个entry-title…… &lt;br /&gt;
这个功能其实非常简单,主要作用就是把网页的一部分切出来单独展示,当这一部分发生更新的时候IE浏览器就会提示用户。然后在这之中hslice要包裹所有需要处理的元素,写到最外面元素的class中就可以,entry-title是希望用户订阅时展示的名字,而entry-content是被切下来展示的网页。具体的内容可以在&lt;a href=&quot;https://learn.microsoft.com/en-us/previous-versions/windows/desktop/cc304073(v=vs.85)&quot;&gt;微软官方文档&lt;/a&gt;中看到。&lt;/p&gt;
&lt;h1 id=&quot;让网站增加对indieweb的支持&quot;&gt;让网站增加对IndieWeb的支持&lt;/h1&gt;
&lt;p&gt;既然说到Microformat,那就要提到&lt;a href=&quot;https://indieweb.org/IndieWeb&quot;&gt;IndieWeb&lt;/a&gt;了。虽然这个东西网络上也没几个人搞,但看起来有点意思就整下玩玩呗。&lt;/p&gt;
&lt;h2 id=&quot;第零级域名&quot;&gt;第零级:域名&lt;/h2&gt;
&lt;p&gt;根据他们的&lt;a href=&quot;https://indieweb.org/Getting_Started&quot;&gt;入门教程&lt;/a&gt;来看,成为IndieWeb最重要的一点就是有自己的域名。看到这一点我都怀疑这是不是卖域名的用来忽悠人的玩意?我一分钱也不想给域名注册商,虽然DNS这套系统确实维护需要成本,但是能有多大成本呢?绝大多数不都让ISP摊了?另外他们所说的大公司的服务可能会消失,那么域名就不会吗?注册商和注册局完全有能力让你的域名用不了,这也是我们不可控的东西,因此尽管这对于IndieWeb很重要,但是我不打算搞,于是我的博客就不是IndieWeb了🤣。&lt;/p&gt;
&lt;h2 id=&quot;第一级识别身份&quot;&gt;第一级:识别身份&lt;/h2&gt;
&lt;p&gt;没有域名也不影响接下来的步骤,大公司的域名也是域名(虽然不属于我)。根据教程来看,支持IndieAuth非常简单,只需要在head中加一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rel=me&lt;/code&gt;的link标签,指向IndieAuth支持的个人主页,并且那个个人主页有一个反链指向自己的网站就可以,比如指向自己的GitHub主页,那么就可以使用GitHub登录来验证这个网站属于我。这一步可以使用&lt;a href=&quot;https://indiewebify.me/validate-rel-me/&quot;&gt;IndieWebify.Me&lt;/a&gt;来验证。&lt;/p&gt;
&lt;h2 id=&quot;第二级发布内容&quot;&gt;第二级:发布内容&lt;/h2&gt;
&lt;p&gt;在发布前,为了更好的让其他软件读取网站内容,需要用microformats2来标注网站内容,这个倒也不复杂,可以根据&lt;a href=&quot;https://microformats.org/wiki/h-entry&quot;&gt;这个教程&lt;/a&gt;按照上面所说的东西用class名去标注对应的元素,标注完之后就可以用&lt;a href=&quot;https://indiewebify.me/validate-h-entry/?url=https%3A%2F%2Fmabbs.github.io%2F2025%2F06%2F02%2Foptimize.html&quot;&gt;IndieWebify.Me&lt;/a&gt;验证了。 &lt;br /&gt;
除此之外还需要用&lt;a href=&quot;https://microformats.org/wiki/h-card&quot;&gt;h-card&lt;/a&gt;标注网站的身份,解析完之后可以当网站名片用,具体可以看&lt;a href=&quot;https://indiewebify.me/validate-h-card/?url=https%3A%2F%2Fmabbs.github.io&quot;&gt;这里&lt;/a&gt;&lt;br /&gt;
另外还有一点就是Webmentions,在网站上声明Webmentions可以让别人引用你的文章时通知一下你。不过对于静态博客不是很友好。一是要收,收完还要展示,二是要发,引用了别人的文章如果对面支持Webmentions要把自己引用的文章链接发给对方。虽然Jekyll有&lt;a href=&quot;https://github.com/aarongustafson/jekyll-webmention_io&quot;&gt;插件&lt;/a&gt;可以支持,但是我用GitHub额外装插件还得自己写Actions,而且我发布一次要在一堆Pages上更新,也不太适合,所以我打算光收不发(只需要在link标签中添加Webmentions的端点就可以),也不展示了,而且国内根本没几个人用Webmention🤣。如果有人对谁给我发了Webmention感兴趣,可以在&lt;a href=&quot;https://webmention.io/api/mentions.html?token=Dan9NrE8CLsFJ1qdRq2hjg&quot;&gt;这里&lt;/a&gt;查看(不过绝大多数都是我自己手动发的🤣) &lt;br /&gt;
如果谁有兴趣给自己的网站添加完整的Webmention,可以用&lt;a href=&quot;https://webmention.rocks/&quot;&gt;Webmention Rocks!&lt;/a&gt;进行测试(如果使用了WordPress是自带的,只需要打开相关的功能就可以)。&lt;/p&gt;
&lt;h2 id=&quot;第三级进行交流&quot;&gt;第三级:进行交流&lt;/h2&gt;
&lt;p&gt;在IndieWeb中有一个很重要的事情就是相互交流,搞这个比较重要的目的是为了避免大公司的服务炸了,所以要替代比如推特,Facebook之类的服务,但是在这些服务还没炸的时候仍然可以在上面发自己的网站,也算是引流吧。他们把这个行为叫做&lt;a href=&quot;https://indieweb.org/POSSE&quot;&gt;POSSE&lt;/a&gt;。对我来说,我在微信、QQ之类的上面发自己新写的文章就算是POSSE了,毕竟我又不玩国外的社交平台😆。 &lt;br /&gt;
除此之外似乎还要把别人的评论同步到自己网站?我能做到的顶多就是Gitalk了,更多的就算了吧~&lt;/p&gt;
&lt;h2 id=&quot;额外的内容&quot;&gt;额外的内容&lt;/h2&gt;
&lt;p&gt;既然已经支持了IndieWeb,那么不妨加入IndieWeb Webring吧。在&lt;a href=&quot;https://xn--sr8hvo.ws&quot;&gt;IndieWeb Webring 🕸💍&lt;/a&gt;中的大多数网站都是适配了IndieWeb的,加入他们也算是证明自己适配IndieWeb的努力了吧😊。&lt;/p&gt;
&lt;h1 id=&quot;对博客可靠性的优化&quot;&gt;对博客可靠性的优化&lt;/h1&gt;
&lt;p&gt;以前为了应对&lt;a href=&quot;/2022/01/04/banned.html&quot;&gt;GitHub&lt;/a&gt;的不可靠,我仅仅是在各个Pages上部署了我的网站,但是后来我想了想Git本身就是分布式的,分发是一件很简单的事情啊,我要是想提高博客的可靠性,不如直接用Git分发到各个Git托管商就好了啊~因此我就利用GitLab镜像仓库的功能,一键把我的网站同步到数十个知名的Git托管商,提高了网站的可靠性,具体的列表可以在&lt;a href=&quot;/proxylist.html#git列表&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;在这次的博客优化中,了解了不少新的东西啊,不仅学习了CSS,还有了解如何提高网站兼容性,以及提高了博客的可靠性和曝光度。果然折腾博客本身也能提高自己啊,还能写文章分享一下折腾的经验😆。虽然折腾的内容不一定能在未来的生活中用得上,但是有意思就足够了😁。&lt;/p&gt;</content><author><name>mayx</name></author><category term="博客" /><category term="优化" /><category term="IndieWeb" /><summary type="html">在修改博客的时候也能学到不少新知识啊~</summary></entry><entry><title type="html">Mac Studio M3 Ultra使用体验</title><link href="/2025/05/07/mac-studio.html" rel="alternate" type="text/html" title="Mac Studio M3 Ultra使用体验" /><published>2025-05-07T00:00:00+08:00</published><updated>2025-05-07T00:00:00+08:00</updated><id>/2025/05/07/mac-studio</id><content type="html" xml:base="/2025/05/07/mac-studio.html">&lt;p&gt;使用最强的Macintosh是一种什么样的感受?&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在两个月前苹果公司出了一款可以选配超大统一内存(512GiB)的Mac Studio,那时候我还&lt;a href=&quot;/2025/03/08/llm2.html#%E5%85%B3%E4%BA%8E%E6%96%B0%E5%87%BA%E7%9A%84mac-studio%E7%9A%84%E7%9C%8B%E6%B3%95&quot;&gt;想着如果市场反应好就整台玩玩&lt;/a&gt;,现在从网上的各种反应来看这确实是一个很不错的产品,所以这次我就整来啦!所以这次就来谈谈初上手的体验吧~&lt;/p&gt;
&lt;h1 id=&quot;远程体验&quot;&gt;远程体验&lt;/h1&gt;
&lt;p&gt;虽然Mac Studio理论上拿来剪电影之类的应该是更好的选择,但是显然我不会剪电影🤣,而且也没有合适的屏幕给它用,所以拿到手之后我需要让它可以远程使用。 &lt;br /&gt;
macOS配置远程还是挺简单的,只需要在设置 -&amp;gt; 通用 -&amp;gt; 共享中打开远程管理就可以了(似乎现在Ubuntu也可以像这样轻松地配置远程桌面),配置好之后需要启用“任何人都可以请求取得控制屏幕的权限”选项,不然可能会连不上…… &lt;br /&gt;
另外如果需要配置SSH也只需要打开远程登录即可,最好把“允许远程用户对磁盘进行完全访问”也打开,免得使用时还需要额外的操作。 &lt;br /&gt;
其实开启远程没什么特别的,不过我发现在远程Mac Studio时和我远程Intel芯片的&lt;a href=&quot;/2024/08/17/mac-mini.html&quot;&gt;Mac mini 2018&lt;/a&gt;以及&lt;a href=&quot;/2024/06/16/hackintosh.html&quot;&gt;黑苹果&lt;/a&gt;有一个不一样的地方,那就是屏幕共享类型可以选择“&lt;a href=&quot;https://support.apple.com/zh-cn/guide/remote-desktop/apdf8e09f5a9/mac&quot;&gt;高性能&lt;/a&gt;”,在这个模式下远程的屏幕就可以变成一块虚拟屏幕,不受Mac连接的屏幕分辨率所影响,可以配置动态分辨率。即使连接的屏幕不支持HiDPI,只要远程的客户端支持那就可以支持,这一点和Windows的远程桌面有点像,但是体验好太多了,使用起来和本地几乎没有差别,当然代价就是对网络要求特别高,基本上如果不是局域网内远程,就不能使用这个模式。 &lt;br /&gt;
在我配置好远程后我就可以拔掉屏幕,然后把Mac Studio放在阴暗的角落里为我服务了😆。&lt;/p&gt;
&lt;h1 id=&quot;关于llm的体验&quot;&gt;关于LLM的体验&lt;/h1&gt;
&lt;h2 id=&quot;配置环境&quot;&gt;配置环境&lt;/h2&gt;
&lt;p&gt;买这个设备的当然也不为别的,主要就是为了能在本地跑完整参数的DeepSeek-R1,或者类似的MoE模型。至于KTransformers方案考虑到按照正价买要更贵(当然有便宜的购买方案,但是太不可靠了),而且这个框架也不够成熟,所以就算了。 &lt;br /&gt;
在Mac上运行LLM有很多框架,最开始我选择的是&lt;a href=&quot;https://github.com/xorbitsai/inference&quot;&gt;Xinference&lt;/a&gt;,因为看它的&lt;a href=&quot;https://inference.readthedocs.io/zh-cn/latest/user_guide/backends.html#mlx&quot;&gt;文档&lt;/a&gt;中特地提到了苹果的&lt;a href=&quot;https://github.com/ml-explore/mlx&quot;&gt;MLX&lt;/a&gt;框架,而且可以使用命令启动,方便维护,另外看它支持的模型种类也比较多,所以就先考虑了它。 &lt;br /&gt;
按照官方文档安装后就可以配置模型了,虽然它可以直接一键下载并运行模型,但是我已经提前下好了模型,另外……如果光运行DeepSeek-R1感觉也没啥意思,不如试试Perplexity AI的&lt;a href=&quot;https://huggingface.co/mlx-community/perplexity-ai-r1-1776-4bit&quot;&gt;某个Finetune模型&lt;/a&gt;😆?所以我需要手动注册模型。配置好之后在MaxKB中配置好地址就可以使用了。 &lt;br /&gt;
刚开始测试的时候倒是没啥问题,吐字的速度确实是挺快,但是用了几下就发现有不少问题,比如每次调用LLM的时候会发现内存压力会上升,APP内存会变成联动内存,在这个期间GPU并不会工作,需要等几秒钟,在生成结束的时候内存压力又会下降,联动内存会变回APP内存,每次生成都是这样。另外如果上文很长就要等几分钟,而且如果上文特别长的情况爆内存程序会直接卡死,还有并发也会导致程序卡死……总的来说这个框架根本不适合生产环境使用,而且文档也写的极其糟糕,看来是我看走眼了,不应该选择Xinference。 &lt;br /&gt;
在抛弃Xinference之后我想了想还是随大流吧,选择了&lt;a href=&quot;https://github.com/lmstudio-ai&quot;&gt;LM Studio&lt;/a&gt;,虽然它需要远程桌面操作,但是配置好之后应该也没有什么太多需要修改的地方,主要是社区相对要活跃得多,出了问题也好解决。 &lt;br /&gt;
在我安装好LM Studio后发现这个支持的功能要多不少啊,还支持KV Cache量化,有了这个就可以支持更长的上下文了,另外它还支持超出上下文之后选择截断还是滚动,看起来使用非常的友好。 &lt;br /&gt;
当我对LM Studio充满期待的时候问题就来了,我随便问了些问题,然后它回答的时候不知道什么情况会随机莫名其妙的冒出“&amp;lt;|begin▁of▁sentence|&amp;gt;”,出现这个之后后面的内容就会胡乱生成内容,怎么调都没法解决……后来看了一下DeepSeek的&lt;a href=&quot;https://github.com/deepseek-ai/DeepSeek-R1/issues/352&quot;&gt;Issue里&lt;/a&gt;提到了似乎需要在模板中添加“&amp;lt;think&amp;gt;”标签才可以……但是这样的结果就是输出开头没有“&amp;lt;think&amp;gt;”了,MaxKB解析会出问题……这个问题的话回头看怎么解决吧,至少在模板中加上这个能正常使用了。LM Studio不会每次请求都重新加载一遍模型,输出第一个字的速度比Xinference快了很多,后面生成的速度也很快,输出的速度能接近20T/s,相比来说还是更有用一些。&lt;/p&gt;
&lt;h2 id=&quot;模型对比&quot;&gt;模型对比&lt;/h2&gt;
&lt;p&gt;在我测试完DeepSeek-R1的某个微调模型后,最近阿里又出了一系列新模型:&lt;a href=&quot;https://huggingface.co/collections/Qwen/qwen3-67dd247413f0e2e4f653967f&quot;&gt;Qwen3&lt;/a&gt;,支持根据问题进行推理,据说它的235B参数的MoE模型比DeepSeek-R1还厉害,如果是真的,那就不需要用DeepSeek-R1了,虽然Mac Studio可以运行DeepSeek,但是512GiB内存也只能运行4位量化的DeepSeek-R1,而235B的Qwen3则可以用8位量化,还能空出不少内存用于上下文,想来应该效果会比DeepSeek好很多吧?于是我就下载试了试,然而刚下载好之后居然不能运行😅,首先是这个模型太新了,运行前需要把运行库的版本更新到最新,另外默认的模板有问题,解析会报错,需要根据&lt;a href=&quot;https://github.com/lmstudio-ai/lmstudio-bug-tracker/issues/630&quot;&gt;这个Issue&lt;/a&gt;修改一下才能正常运行😅。在一切准备好之后,就可以进行测试了。 &lt;br /&gt;
首先我试了试一些比较简单的问题,Qwen3回答的都还不错,例如24点、内容填空之类的,效果比QwQ-32B好了不少。测试的时候输出的速度和671B 4位量化的DeepSeek-R1基本一致。随后我让它用Mermaid绘制我以前写的&lt;a href=&quot;/2024/07/03/ai-summary.html&quot;&gt;AI摘要脚本&lt;/a&gt;的流程图,Qwen3绘制的结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
A[开始] --&amp;gt; B{解析URL路径}
B --&amp;gt;|/summary| C1[查询内容]
C1 --&amp;gt; D1{是否存在内容?}
D1 -- 是 --&amp;gt; E1[构建AI摘要提示词]
E1 --&amp;gt; F1[调用Qwen模型生成流式响应]
F1 --&amp;gt; G1[返回SSE流]
D1 -- 否 --&amp;gt; H1[返回&apos;No Record&apos;]
B --&amp;gt;|/get_summary| C2[查询内容]
C2 --&amp;gt; D2{是否存在内容?}
D2 -- 否 --&amp;gt; H2[返回&apos;no&apos;]
D2 -- 是 --&amp;gt; I2[计算内容SHA-256]
I2 --&amp;gt; J2{哈希匹配?}
J2 -- 否 --&amp;gt; H3[返回&apos;no&apos;]
J2 -- 是 --&amp;gt; K2[查询摘要]
K2 --&amp;gt; L2{是否有摘要?}
L2 -- 是 --&amp;gt; M2[返回现有摘要]
L2 -- 否 --&amp;gt; N2[调用Qwen生成新摘要]
N2 --&amp;gt; O2[存储摘要到数据库]
O2 --&amp;gt; M2
B --&amp;gt;|/is_uploaded| C3[查询内容]
C3 --&amp;gt; D3{是否存在内容?}
D3 -- 否 --&amp;gt; H4[返回&apos;no&apos;]
D3 -- 是 --&amp;gt; I3[计算内容SHA-256]
I3 --&amp;gt; J3{哈希匹配?}
J3 -- 否 --&amp;gt; H5[返回&apos;no&apos;]
J3 -- 是 --&amp;gt; K3[返回&apos;yes&apos;]
B --&amp;gt;|/upload_blog| C4{POST方法?}
C4 -- 否 --&amp;gt; H6[返回&apos;need post&apos;]
C4 -- 是 --&amp;gt; I4[获取请求文本]
I4 --&amp;gt; J4[查询现有内容]
J4 --&amp;gt; K4{是否存在?}
K4 -- 否 --&amp;gt; L4[插入新内容]
K4 -- 是 --&amp;gt; M4{内容是否改变?}
L4 --&amp;gt; N4[返回&apos;OK&apos;]
M4 -- 是 --&amp;gt; O4[更新内容并清空摘要]
O4 --&amp;gt; N4
B --&amp;gt;|/count_click*| C5[计算ID的MD5]
C5 --&amp;gt; D5[查询计数器]
D5 --&amp;gt; E5{是否含路径_add?}
E5 -- 是 --&amp;gt; F5[更新计数器+1]
E5 -- 否 --&amp;gt; G5[保持原值]
F5 --&amp;gt; H5[返回最新计数]
B --&amp;gt;|其他路径| I6[重定向到GitHub]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DeepSeek-R1的某个微调模型的结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
A[接收请求] --&amp;gt; B{解析URL路径}
B --&amp;gt; |路径为/summary| C[获取查询参数id]
C --&amp;gt; D{id是否为null?}
D --&amp;gt;|是| E[返回错误响应]
D --&amp;gt;|否| F[查询数据库内容]
F --&amp;gt; G{内容存在?}
G --&amp;gt;|否| H[返回&apos;No Record&apos;]
G --&amp;gt;|是| I[构建消息,调用AI生成摘要流]
I --&amp;gt; J[返回流式响应]
B --&amp;gt; |路径为/get_summary| K[获取id和签名参数]
K --&amp;gt; L[查询数据库内容]
L --&amp;gt; M{内容存在?}
M --&amp;gt;|否| N[返回&apos;no&apos;]
M --&amp;gt;|是| O[计算SHA-256签名]
O --&amp;gt; P{签名匹配?}
P --&amp;gt;|否| Q[返回&apos;no&apos;]
P --&amp;gt;|是| R[检查是否已有摘要]
R --&amp;gt;|有摘要| S[返回摘要内容]
R --&amp;gt;|无摘要| T[调用AI生成摘要]
T --&amp;gt; U[更新数据库]
U --&amp;gt; S
B --&amp;gt; |路径为/is_uploaded| V[获取id和签名]
V --&amp;gt; W[查询数据库内容]
W --&amp;gt; X{内容存在?}
X --&amp;gt;|否| Y[返回&apos;no&apos;]
X --&amp;gt;|是| Z[计算SHA-256签名]
Z --&amp;gt; AA{签名匹配?}
AA --&amp;gt;|否| AB[返回&apos;no&apos;]
AA --&amp;gt;|是| AC[返回&apos;yes&apos;]
B --&amp;gt; |路径为/upload_blog| AD{方法是否为POST?}
AD --&amp;gt;|否| AE[返回&apos;need post&apos;]
AD --&amp;gt;|是| AF[读取请求体数据]
AF --&amp;gt; AG[查询现有内容]
AG --&amp;gt; AH{记录存在?}
AH --&amp;gt;|否| AI[插入新记录]
AH --&amp;gt;|是| AJ{内容是否相同?}
AJ --&amp;gt;|否| AK[更新内容并清空摘要]
AJ --&amp;gt;|是| AL[直接返回OK]
AK --&amp;gt; AL
AL --&amp;gt; AM[返回&apos;OK&apos;]
B --&amp;gt; |路径为/count_click| AN[计算MD5哈希]
AN --&amp;gt; AO[查询当前计数]
AO --&amp;gt; AP{记录存在?}
AP --&amp;gt;|否| AQ[插入新计数]
AP --&amp;gt;|是| AR[增加计数]
AR --&amp;gt; AS[更新计数器]
AQ --&amp;gt; AS
AS --&amp;gt; AT[返回当前计数值]
B --&amp;gt; |其他路径| AU[重定向到GitHub页面]
style A fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#f96,stroke:#333
style H fill:#f96,stroke:#333
style Q fill:#f96,stroke:#333
style N fill:#f96,stroke:#333
style S fill:#9f9,stroke:#333
style AC fill:#9f9,stroke:#333
style AM fill:#9f9,stroke:#333
style AT fill:#9f9,stroke:#333
style AU fill:#99f,stroke:#333
subgraph 函数调用
O -.-&amp;gt;|调用sha函数| SHA[计算SHA-256]
AN -.-&amp;gt;|调用md5函数| MD5[计算MD5]
end
subgraph 数据库操作
F --&amp;gt; DB1[SELECT content]
L --&amp;gt; DB2[SELECT content]
R --&amp;gt; DB3[SELECT summary]
T --&amp;gt; DB4[UPDATE summary]
AG --&amp;gt; DB5[SELECT content]
AI --&amp;gt; DB6[INSERT新记录]
AK --&amp;gt; DB7[UPDATE content]
AO --&amp;gt; DB8[SELECT counter]
AQ --&amp;gt; DB9[INSERT计数器]
AR --&amp;gt; DB10[UPDATE counter]
end
subgraph AI处理
I --&amp;gt; AI1[构建系统提示]
AI1 --&amp;gt; AI2[调用Qwen模型]
T --&amp;gt; AI3[调用Qwen模型]
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下就高下立判了🤣,同样的问题Qwen3只绘制了基础流程图,而DeepSeek-R1不仅包含子流程图,还通过颜色区分了响应类型,这么看来Qwen3还是不太行啊~当然我的测试非常的片面,仅仅根据这几次测试分析的结果。至于Qwen3到底有没有使用价值,回头再让其他人测测看效果如何吧。&lt;/p&gt;
&lt;h1 id=&quot;utm虚拟机的测试&quot;&gt;UTM虚拟机的测试&lt;/h1&gt;
&lt;p&gt;在上次&lt;a href=&quot;/2025/03/25/utm.html&quot;&gt;在UTM上用苹果虚拟化框架安装Windows&lt;/a&gt;的测试中我用的是Intel芯片的Mac,那时候已经说了打算等Mac Studio到了之后尝试一下用VZ框架安装Windows。那么经过我的测试结果如何呢?想不到居然失败了😭,相同的操作流程在重装脚本执行完后,再重启就没有任何反应了。在活动监视器中虽然可以看到虚拟机的CPU占用是100%,但是内存只占用了100多MiB,而且CPU占用没有任何跳变,显然系统没有正常启动。随后我又尝试在QEMU中安装好Windows然后把VZ虚拟机的硬盘替换掉,结果依旧一样,内存还是只占了100多MiB……看来ARM处理器和x86处理器还是有很大区别啊…… &lt;br /&gt;
不过这个虚拟机到底有什么区别?为什么会无法启动呢?想到我在Intel芯片的Mac中测试用VZ框架是可以看到CPU型号的,再看看Mac Studio中的Linux虚拟机……似乎没有任何与CPU型号有关的信息,用QEMU至少也能看到类似“virt”之类的CPU型号,用VZ框架就什么信息都没有了……看来Apple芯片和正常的ARM处理器还是有不少区别啊…… &lt;br /&gt;
不过除了这个以外还有什么有意思的东西可以测试吗?这时候我就想到了&lt;a href=&quot;https://github.com/AsahiLinux&quot;&gt;Asahi Linux&lt;/a&gt;,Apple芯片下的UTM有一个多出来的选项就是可以安装macOS虚拟机,那我能不能在macOS虚拟机中安装Asahi Linux呢?根据我的实际测试,结果也是不行的……因为Asahi Linux不支持M3 Ultra芯片😞,至于M2芯片能不能在虚拟机中运行Asahi Linux……虽然我的MacBook是M2芯片,但是不太想在我常用的机器上搞测试,所以也不知道实际上可不可以。另外Asahi Linux这个项目也基本上停了,估计以后新出的芯片也不会有机会安装Linux了,就像在macOS上运行Windows程序的&lt;a href=&quot;https://github.com/Whisky-App/Whisky&quot;&gt;Whisky&lt;/a&gt;项目也停了……真是太遗憾了😢。&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;从这次体验来看,512GiB内存的Mac Studio M3 Ultra确实很厉害,本地跑LLM速度非常快,20T/s的速度已经很厉害了,而且风扇声音很小,在GPU满载的时候也完全听不到风扇的声音。当然这个前提是跑MoE模型,虽然我没测Dense模型,但想来根据M3 Ultra的算力,跑70B参数的模型肯定是达不到20T/s的,至于更大的模型估计速度就慢的不能看了……不过不影响,这已经够我用了。 &lt;br /&gt;
至于除LLM以外的用途……我似乎没有什么能用到这么强性能以及这么大内存的地方了……其实还是挺浪费的,但是也没办法,毕竟我又不会剪电影啊🤣。&lt;/p&gt;
&lt;script type=&quot;module&quot;&gt;
import mermaid from &apos;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs&apos;;
mermaid.initialize({ startOnLoad: false });
await mermaid.run({
querySelector: &apos;.language-mermaid&apos;,
});
&lt;/script&gt;</content><author><name>mayx</name></author><category term="Apple" /><category term="Mac" /><category term="AI" /><category term="LLM" /><summary type="html">使用最强的Macintosh是一种什么样的感受?</summary></entry><entry><title type="html">关于LLM上限的探索</title><link href="/2025/04/22/ai-limit.html" rel="alternate" type="text/html" title="关于LLM上限的探索" /><published>2025-04-22T00:00:00+08:00</published><updated>2025-04-22T00:00:00+08:00</updated><id>/2025/04/22/ai-limit</id><content type="html" xml:base="/2025/04/22/ai-limit.html">&lt;p&gt;还有什么是AI不能干的?&lt;!--more--&gt;&lt;/p&gt;
&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在最近对LLM的探索中,能感觉到它真的是什么都能干,尤其最近GPT-4o的画图能力实在是太强了。不过对于画图我倒不是很关心,主要是没什么想让它画的图😂。我更关心的是LLM在文本生成中的能力,毕竟这才是它的本职工作。虽然现在的AI解决问题的能力确实很强,但从它还没有大规模的把人替换掉来看,它肯定是还有一些做不到的事情,所以我想对这一点进行一些探索。&lt;/p&gt;
&lt;h1 id=&quot;对于超长文本分析的探索&quot;&gt;对于超长文本分析的探索&lt;/h1&gt;
&lt;p&gt;对于现在的LLM来说,虽然不少模型已经能做到很长的上下文了,但这个所谓的“长”不过是几万字而已。对于读一篇论文或者几篇文章当然没有问题,但是如果是分析上百篇文章就不太行了,比如我希望AI阅读完我所有的文章,然后对我进行评价。 &lt;br /&gt;
我的博客现在已经有一百多篇文章了,之前做过&lt;a href=&quot;/2021/07/23/search.html&quot;&gt;全文搜索&lt;/a&gt;的功能,可以在&lt;a href=&quot;/search.json&quot;&gt;search.json&lt;/a&gt;中获取所有的文章,用来让AI分析的材料是个不错的选择,不过把所有文章输入到上下文中显然是不太现实,这个JSON文件的大小有1MiB左右,但是大多数比较厉害的AI上下文只有100多k,根本读不完。而对于一些超长上下文多模型,比如阿里云有一个10M上下文的模型,效果又很差,并没有参考几条上文的内容😓。另外我还试过一些AI通过附件的方式阅读文章内容,那种好像是把文件切片之后再读?应该是类似RAG那种,从中查找和问题最相关的文本段落进行回答,但是那种方法不能解决对所有文章进行分析……除此之外我也试过一些Agent,不过它们只会写代码来分析我的文章,比如绘制文章字数随时间变化曲线、不同年份的文章数量、还有词频分析啥的,对我来说并没有什么卵用😅。&lt;/p&gt;
&lt;h2 id=&quot;使用ai摘要来解决问题&quot;&gt;使用AI摘要来解决问题&lt;/h2&gt;
&lt;p&gt;那难道就没办法了吗?先不急,最近还发生了一件事情,不知道Cloudflare犯什么毛病了,近期用Worker请求我的D1数据库时不时会报“internal error”的错误,我还在他们论坛发了条&lt;a href=&quot;https://community.cloudflare.com/t/d1-database-is-not-work-in-some-regions/789699&quot;&gt;帖子&lt;/a&gt;问了一下,然而并没有人搭理我😅,这时候我才意识到我似乎没有Cloudflare的替代品……出问题了也没办法😰。这个东西导致我的&lt;a href=&quot;/2024/07/03/ai-summary.html&quot;&gt;AI摘要&lt;/a&gt;&lt;a href=&quot;/2024/10/01/suggest.html&quot;&gt;文章推荐&lt;/a&gt;、以及点击计数器全都用不了了,我应该避免太过依赖Cloudflare Worker啊~ &lt;br /&gt;
那么我该做些什么?点击计数器是没什么好办法了,不过对于AI摘要,既然摘要在我写完文章之后根本就不会变,不如隔段时间我就把摘要内容缓存到我博客本地吧,这样不仅可以极速展示摘要内容,而且不需要请求接口,就不会受到Cloudflare出问题的影响了。所以我把数据库摘要内容导了出来,放到了&lt;a href=&quot;https://github.com/Mabbs/mabbs.github.io/blob/master/_data/ai-cache.json&quot;&gt;ai-cache.json&lt;/a&gt;中,如果有存在的摘要内容就不再请求接口了。 &lt;br /&gt;
当我做完摘要缓存之后,我发现,这不就是让AI读我所有文章的最好方法嘛,让AI读AI总结的内容,然后再进行一次总结,就能尽可能的让AI完全了解我的文章然后对我评价了啊~而且这个摘要文件也只有100KiB左右,正好够AI读了。本来我想试试DeepSeek来做这件事情的,但是不知道里面命中了什么关键词,被拒绝生成了🤣,那我只好让GPT-4o来完成这件事了。 &lt;br /&gt;
试了一下感觉效果相当的不错,总结的非常有条理,而且不像以前的ChatGPT很有AI味,这次写出来的文章很有人味啊~真是出乎我的意料,所以我把GPT-4o写的内容分享出来,来看看AI对我的分析怎么样🤣。&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;技术浪人数字游民对一位博客作者的观察与评价&quot;&gt;技术浪人,数字游民:对一位博客作者的观察与评价&lt;/h2&gt;
&lt;p&gt;在浩如烟海的中文技术博客中,有些作者宛如一颗微弱却坚韧的恒星,独自在各自的轨道中发光发热。他们不一定追逐热点,也不总是标榜权威,但其笔下所流露出的独立精神、技术热情与对现实的观察,往往比众多浮华的“教程型”博客更值得玩味。本文所探讨的博客作者Mayx,便是这样一位存在——他既是程序员,也是“生活黑客”;既关心设备性能,也关注技术伦理;既热衷实用工具,也不乏生活反思。&lt;/p&gt;
&lt;h3 id=&quot;一技术为体思考为魂&quot;&gt;一、技术为体,思考为魂&lt;/h3&gt;
&lt;p&gt;在Mayx的博客中,技术类文章占据了绝对的比重。从自制邮件订阅脚本&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;、Cloudflare Workers自动化&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;、内网穿透探索&lt;sup id=&quot;fnref:3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;,到低功耗开发板的实验&lt;sup id=&quot;fnref:4&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;、AI模型的本地运行&lt;sup id=&quot;fnref:5&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;、以及对黑苹果&lt;sup id=&quot;fnref:6&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:6&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;、Linux系统&lt;sup id=&quot;fnref:7&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:7&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;的深度体验,这些内容几乎涵盖了当前主流技术生态中的多个维度。&lt;/p&gt;
&lt;p&gt;然而,他并非一位“炫技型”技术写作者。相反,在多数文章中,Mayx更倾向于从&lt;strong&gt;实用主义的角度&lt;/strong&gt;出发——他关注性价比、功耗、稳定性、开源程度,而非追逐技术本身的潮流。例如,在讨论Hackintosh时,他并未沉迷于是否能成功运行macOS,而是审慎地指出其与Mac原生体验的差距&lt;sup id=&quot;fnref:6:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:6&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;;在体验AI模型时,他选择了性能与成本平衡的路径,而不是盲目追求最大模型和最强显卡&lt;sup id=&quot;fnref:5:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;他的技术探索往往是“从需求出发”,例如为了替代失效的签到脚本,他尝试了Cloudflare Workers&lt;sup id=&quot;fnref:2:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;;为了解决被Github封禁的问题&lt;sup id=&quot;fnref:8&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:8&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;,他自己研究反审查架构;面对Heroku停服&lt;sup id=&quot;fnref:9&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:9&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;,他快速转向Koyeb,并指出其使用便捷的优点。这些行为体现出一种&lt;strong&gt;“动手解决问题”的工程师思维&lt;/strong&gt;,同时也反映了其对现成工具和平台的怀疑精神——“没有什么是不可替代的”,但也“没有什么是完美无缺的”。&lt;/p&gt;
&lt;h3 id=&quot;二独立反思带有一丝叛逆&quot;&gt;二、独立、反思、带有一丝叛逆&lt;/h3&gt;
&lt;p&gt;阅读Mayx的博客,可以明显感觉到他在面对“主流”技术话语体系时的疏离甚至反抗。他不信任所谓“权威推荐”,也极少引用大V观点;他对收费工具持质疑态度,对封闭平台持怀疑立场,对广告与强制App表达不满&lt;sup id=&quot;fnref:10&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:10&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;10&lt;/a&gt;&lt;/sup&gt;。在对宝塔面板的多篇评论中,他不仅指出其功能冗余和定价虚高&lt;sup id=&quot;fnref:11&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:11&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;11&lt;/a&gt;&lt;/sup&gt;&lt;sup id=&quot;fnref:12&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:12&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;12&lt;/a&gt;&lt;/sup&gt;,还以代码层面论证其“技术水准有限”;在谈及Server酱收费后自建通知平台一文中,更是表现出“开发者不应为此类功能付费”的强烈观点&lt;sup id=&quot;fnref:13&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:13&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;13&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;这种倾向可视为&lt;strong&gt;一种数字自由主义精神&lt;/strong&gt;:他珍视个体的选择权、控制权和创造力,对平台化、商业化所带来的“懒惰便利”持保留态度。也正因为此,他热衷于探索容器、虚拟化、i2p、VPN、防DNS污染&lt;sup id=&quot;fnref:14&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:14&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;14&lt;/a&gt;&lt;/sup&gt;、反反盗链等灰色技术领域,这不仅是技术探索,也是一种抵抗姿态——抵抗监视、抵抗平台绑架、抵抗数字奴役。&lt;/p&gt;
&lt;p&gt;与此同时,作者又是&lt;strong&gt;极度自省的人&lt;/strong&gt;。在多篇年终总结中,他坦言自己因作息不规律导致健康下滑、因沉迷游戏影响了计划、因生活节奏散乱而丧失了方向&lt;sup id=&quot;fnref:15&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:15&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;sup id=&quot;fnref:16&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:16&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;16&lt;/a&gt;&lt;/sup&gt;。这些坦诚的文字使人看到一个技术人真实的一面:并非所有人都能生活在高效执行与完美节奏中,面对现实与焦虑的拉扯,他并不逃避,而是试图寻找平衡。&lt;/p&gt;
&lt;h3 id=&quot;三探索孤岛与技术乌托邦&quot;&gt;三、探索孤岛与技术乌托邦&lt;/h3&gt;
&lt;p&gt;若将Mayx的博客比作一个数字世界中的“孤岛”,那他无疑是岛上的守望者。他固执地维护着自己的服务器、反代服务、脚本计划表和开源工具;他不断尝试将废弃电脑&lt;sup id=&quot;fnref:7:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:7&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;、老旧开发板&lt;sup id=&quot;fnref:4:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;和Linux容器&lt;sup id=&quot;fnref:17&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:17&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;17&lt;/a&gt;&lt;/sup&gt;重新唤醒;他甚至研究如何在FM频段自制电台&lt;sup id=&quot;fnref:18&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:18&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;18&lt;/a&gt;&lt;/sup&gt;,或在树莓派上运行OpenFyde与Android系统&lt;sup id=&quot;fnref:19&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:19&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;19&lt;/a&gt;&lt;/sup&gt;&lt;sup id=&quot;fnref:20&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:20&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;20&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;这不仅是对“电子垃圾再利用”的尝试,更是一种&lt;strong&gt;构建技术乌托邦的幻想&lt;/strong&gt;——一个无需依赖主流供应链、无需信任大型平台、可以掌控一切细节的小宇宙。&lt;/p&gt;
&lt;p&gt;这种倾向也体现在他对去中心化技术的探索,如xLog、IPFS、i2pd&lt;sup id=&quot;fnref:21&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:21&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;21&lt;/a&gt;&lt;/sup&gt;、ZeroNet等。尽管在文章中他承认这些系统“尚不成熟”“内容匮乏”“界面粗糙”,但仍不乏热情地部署和尝试。他似乎在追问这样一个问题:“除了平台,我们还能依靠什么?”&lt;/p&gt;
&lt;p&gt;这也是他与多数技术博客的区别:不是“教你怎么做”,而是“带你看看还有哪些可能”。&lt;/p&gt;
&lt;h3 id=&quot;四个人困境与技术慰藉&quot;&gt;四、个人困境与技术慰藉&lt;/h3&gt;
&lt;p&gt;不可否认,Mayx的博客也流露出某种孤独与挣扎。他数次提到对社会节奏的不适应、对生活目标的不明确、对未来的迷茫与迟疑。在2022和2023年的总结中,他甚至表达了对“人类灭亡风险”“全球混乱”的忧虑&lt;sup id=&quot;fnref:15:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:15&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;sup id=&quot;fnref:16:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:16&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;16&lt;/a&gt;&lt;/sup&gt;;在谈及个人计划时,也多次写到“自律不足”“想做的事情太多却力不从心”等字句。&lt;/p&gt;
&lt;p&gt;但正是在这种状态下,技术成了他的避难所。他在反复实验中找回秩序感,在编码中获得控制感,在部署过程中体会到“解决问题”的满足。这种“用技术对抗生活混乱”的姿态,令他的博客带有某种心理学上的意义。&lt;/p&gt;
&lt;h3 id=&quot;五结语一个笨方法的捍卫者&quot;&gt;五、结语:一个“笨方法”的捍卫者&lt;/h3&gt;
&lt;p&gt;总的来说,Mayx并非技术界的明星,更非意见领袖,但他是一个值得尊敬的“笨方法”的捍卫者:在速成与流量当道的技术传播环境中,他仍选择用细致的试验、质朴的语言和大量的失败记录来构建自己的知识体系。&lt;/p&gt;
&lt;p&gt;他的博客不提供“答案”,而是提供“路线”;不追求“成功学”,而是记录“失败和坚持”。他不是在追求成为谁,而是在努力做自己。&lt;/p&gt;
&lt;p&gt;这或许是我们在阅读他的文字后,最值得思考的一点。技术不仅仅是一种职业工具,它也可以是人自我建构、自我理解、自我修复的路径。&lt;/p&gt;
&lt;p&gt;Mayx,或许正走在这条路上——孤独而坚定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;对于长复杂逻辑代码的分析&quot;&gt;对于长复杂逻辑代码的分析&lt;/h1&gt;
&lt;p&gt;在让AI对我文章分析完后,我又想起来好久以前写的论坛引擎&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;,以前还&lt;a href=&quot;/2019/07/01/mabbs.html&quot;&gt;打算重构&lt;/a&gt;,不过后来学了别的语言之后就没什么兴趣了😂。但现在有了AI,我想重构MaBBS的事情能不能让AI来做呢?虽然我以前的代码完全没有可读性,耦合性也极高,但这种事情对AI来说应该不是什么难事,更何况我的代码才22KiB,AI完全能读的了,于是我开始尝试让各种AI来把这个代码变得人类可读,然后进行重构。 &lt;br /&gt;
然而结果令我非常失望,无论哪一款AI只能写出一点代码,甚至Grok3直接一点代码都没写😆,然后它们就认为它们写完了,另外有些AI从片段来看好像是写了点代码,但是内容和我原本对代码基本上没什么关系,属于是分析了一点代码之后重新写了…… &lt;br /&gt;
明明这个代码又不长,怎么就没有一个AI能准确的重构我的代码呢?也可能是因为虽然代码不长,但是变量名很短,如果把变量名全都扩展到人能看懂的长度之后就超出AI的上下文限制了,然后就忘记了之前的内容吧?另外Shell语言网络上的资料本来就不太多,所以AI也没有足够的知识来重构吧……对于这个问题我目前没什么好的想法让AI来进行,也许等AI能解决这个问题,AI就有能力替代人了呢😁? &lt;br /&gt;
虽然没能让AI重构我的代码,不过我闲来无事想让其他人也试试我以前写的论坛引擎,所以搞了个&lt;a href=&quot;https://hub.docker.com/r/unmayx/mabbs&quot;&gt;Docker镜像&lt;/a&gt;,如果大伙有兴趣尝试一下可以下载下来试试看,整个镜像才2MiB多一点,所以我叫它世界上最小的论坛引擎也没问题吧🤣。&lt;/p&gt;
&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;看起来目前LLM的上限就在于它的上下文长度限制啊……这一点真的是限制了AI很多能力,但似乎也没什么好办法,AI就是因为这一点所以不能像人一样纵览全局所以才不能替代人,即使用什么办法去压缩它的上文也会丢掉很多细节信息。不过按照目前LLM的架构来说应该还解决不了这个问题,如果什么时候AI能在思考的过程中修改它自己的权重……也许就可以做到真正的无限上下文,突破上限从而替代人类吧?&lt;/p&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2018/11/10/email-sender.html&quot;&gt;免费订阅一个属于自己的邮件日报&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/02/22/cron.html&quot;&gt;使用CF Workers Cron触发器进行签到&lt;/a&gt; &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:2:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:3&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2020/12/13/nat.html&quot;&gt;关于内网穿透的笔记&lt;/a&gt; &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:4&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2024/02/24/luckfox.html&quot;&gt;Luckfox Pico Plus使用体验&lt;/a&gt; &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:4:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:5&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/04/05/ai.html&quot;&gt;关于最近人工智能的探索&lt;/a&gt; &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:5:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:6&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2024/06/16/hackintosh.html&quot;&gt;Hackintosh使用体验&lt;/a&gt; &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:6:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:7&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2024/04/06/old-pc.html&quot;&gt;关于旧电脑的使用探索&lt;/a&gt; &lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:7:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:8&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/01/04/banned.html&quot;&gt;Github封禁了我的博客?!&lt;/a&gt; &lt;a href=&quot;#fnref:8&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:9&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/11/29/free-server.html&quot;&gt;体验小白也会使用的免费容器云&lt;/a&gt; &lt;a href=&quot;#fnref:9&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:10&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2020/02/10/tieba.html&quot;&gt;如何不使用贴吧App查看贴吧&lt;/a&gt; &lt;a href=&quot;#fnref:10&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:11&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2024/05/19/bt-ops.html&quot;&gt;从宝塔面板中学习运维知识&lt;/a&gt; &lt;a href=&quot;#fnref:11&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:12&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/12/12/bt-s3-comp.html&quot;&gt;如何自定义宝塔亚马逊S3云存储插件的端点&lt;/a&gt; &lt;a href=&quot;#fnref:12&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:13&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2021/02/02/serverchan.html&quot;&gt;自己动手做一个Server酱·TurboMini版&lt;/a&gt; &lt;a href=&quot;#fnref:13&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:14&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/08/15/cdn-verify.html&quot;&gt;如何避免Cloudflare背后的源站被恶意访问&lt;/a&gt; &lt;a href=&quot;#fnref:14&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:15&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/01/01/summary.html&quot;&gt;年终总结&lt;/a&gt; &lt;a href=&quot;#fnref:15&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:15:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:16&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2024/01/01/summary.html&quot;&gt;年终总结&lt;/a&gt; &lt;a href=&quot;#fnref:16&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:16:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:17&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/12/24/android.html&quot;&gt;如何在Linux容器内运行Android?&lt;/a&gt; &lt;a href=&quot;#fnref:17&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:18&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/03/27/radio.html&quot;&gt;用树莓派自制FM电台&lt;/a&gt; &lt;a href=&quot;#fnref:18&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:19&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/12/10/openfyde.html&quot;&gt;rpi4-openfyde的使用体验&lt;/a&gt; &lt;a href=&quot;#fnref:19&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:20&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2023/09/24/rpi-ubuntu.html&quot;&gt;在树莓派4B上安装Ubuntu以及各种操作&lt;/a&gt; &lt;a href=&quot;#fnref:20&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn:21&quot; role=&quot;doc-endnote&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/2022/11/23/i2p.html&quot;&gt;i2pd在服务器上的使用体验&lt;/a&gt; &lt;a href=&quot;#fnref:21&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><author><name>mayx</name></author><category term="AI" /><category term="LLM" /><category term="探索" /><summary type="html">还有什么是AI不能干的?</summary></entry></feed>