一次找回GitHub上被删除仓库的经历

在GitHub中寻找踪迹也许是非常简单的事情…… 起因 前段时间,有人和我聊天的时候提到了Brainfuck语言,让我回想起了高中时写的演讲稿。那时候我在演讲时也介绍了Brainfuck语言。对于Brainfuck的解释器,各种语言都可以实现,不过我当时为了方便理解用了一个在GitHub Pages上的网站,用可视化的方式演示了它的运行过程,效果很不错。现在既然聊到了,自然就想分享一下这个演示的网站,但我正想打开时,发现网站已经404了😰。 首页都打不开,看样子是完全退出GitHub了……那么我想找到这个网站的想法就无法实现了吗?不过GitHub有些有意思的特性也许能帮助我找回这个网站。 GitHub的特性 在GitHub中,一个普通的仓库可能没有什么特别的,也许就是服务器上的一个文件夹。但是当仓库被其他人Fork的时候就不一样了,在执行Fork时,显然GitHub不会完整复制整个仓库。否则,同一个仓库在服务器上会占用双倍空间,这显然不合理。另外,想想Git的结构:它由提交对象和分支指针构成,每次提交都有唯一的Hash值且不会冲突。因此可以推测,GitHub在实现Fork时,所有被Fork的仓库可能共享同一个对象库,而每个用户仓库只保存指针,这样所有仓库只会占用增量空间,而不会存储重复内容。 Linux内核仓库做个示例。 提交的Hash值(顺便一说只要值唯一,和其他的提交不冲突,短的Hash值也可以),果不其然能找到刚刚修改的内容,这样一来,只要GitHub和任意一个Linux仓库的Fork还存在,这个提交就永远存在了😝。 找回仓库 那么接下来找回之前网站的方案就很简单了,我只要找到网站仓库的任意一个Fork,然后只要知道最新的提交Hash,我就可以还原最新的仓库了。Fork倒是好找,随便搜一下就能找到一个。这个Fork的最新提交是2016年,但要想找到我当年演讲的版本至少到2018年之后。不过这个Hash值也不太好找,虽然理论上爆破短Hash值也可以,但是感觉太麻烦了,没有那个必要,所以我干脆直接去互联网档案馆看看能找到的最新的仓库页面吧,这样我就能找到它的Hash值了,然后我再把Fork仓库的地址和Hash拼到一起,就看得到最新代码了。 git fetch origin <commit-hash> git reset --hard <commit-hash> git push origin master 最终我就获得了包含最新代码的Brainfuck可视化演示了🎉。 结局 后来我才知道,原来有一个专门的组织Software Heritage会保存所有代码,根本没必要搞这些花里胡哨的操作😂,像这个仓库也是能很轻易在上面找到,这下以后知道了,再遇到类似情况就可以直接去Software Heritage查找,而不必在互联网档案馆上找线索瞎折腾了🤣。

2025/10/12
articleCard.readMore

关于ZIP Quine与自产生程序的探索

描述自己的代码……是一种什么样的感觉? 起因 前段时间我在折腾博客部署的时候,回顾起了好久以前写的部署脚本。对于全站打包的这个步骤,本来我打算利用这个压缩包结合Service Worker做离线浏览,但因为没有合适的方案所以放弃了。而现在对于这个压缩包,我又有了一个特别的想法。事实上在这个下载全站的压缩包中,里面的内容和实际的网站并不完全相同,因为在这个压缩包里缺少了压缩包本身。所以把这个压缩包解压之后直接当作网站打开,会发现下载压缩包的链接是无效的,除非在解压之后把压缩包移动到网站里才行…… 自包含压缩包的探索 在很久之前,我见到过一个很知名的自包含压缩包(又称为ZIP Quine),叫做droste.zip,是由Erling Ellingsen在2005年制作出来的。当时我只知道它很神奇,原理什么的并不清楚,另外在网上也基本上找不到类似的压缩包。现在再回看时发现介绍里包含了一些相关的链接,甚至还有一篇能自己制作类似压缩包的论文,所以接下来就可以看一下这些链接来理解这种压缩包是如何制作的了。 Will Greenberg制作的一个示例,在这里面有一个谜题,使用“print M”(原样输出接下来的M行输入内容)和“repeat M N”(从倒数第N行的输出内容开始,重复M行)这两个指令让最终执行的结果和输入的指令完全相同。这正是对DEFLATE压缩算法所使用的LZ77编码的一种简化模拟,也就是说只要解决了这个问题,就可以让压缩包在解压时原样输出自己了。 Issues就有人给出了几种解法(当然,这个题目解法不唯一),所以在理论上应该是可行的,那么接下来就需要研究压缩文件的格式来实现它了。 实现ZIP Quine的探索 在Russ Cox写的《Zip Files All The Way Down》文章中,同样说明了这个原理,而且给出了一个方案,让上述这两个命令除了能够对命令本身的重复以外,还可以添加一些额外数据,这样才能做到构建一个压缩包文件。按照文章的描述,如果用之前谜题的规则来说,我们设头和尾的内容都是“print 0”,那么Cox给出的方案如下: 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 我们把这些指令粘贴到quine.zip这个谜题中,就会发现输出和输入完全相同,以此就能验证Cox方案的正确性。除此之外作者还给出了生成的源代码:rgzip.go,只是代码里面到处都是用来构建压缩包的十六进制数字,完全看不懂😂。 Matthew Barber写了一篇很棒的文章,通过动画演示并详细讲解了如何实现一个简单的GZIP版ZIP Quine,很值得一看。 repeat 64 64这样的指令能够满足要求,因此头尾区最多只能放64-5=59个字节的数据,也就刚刚好能容纳压缩格式需要的内容,几乎没法塞更多东西进去……显然,这些限制导致这种方式对我来说意义就不大了,何况作者的代码我也看不懂……而且还要考虑压缩包还存在校验用的CRC32,需要找满足整个压缩包的CRC32正好在压缩包中的“不动点”。虽然从CRC32的原理来说应该有办法做到通过数学方式解决,但这篇文章的作者因为解决了自包含的问题之后累了,因此放弃继续研究,选择直接暴力破解,毕竟CRC32只有32位,估计思考的时间都要比爆破的时间长吧😂。但如果是这样,即使有方案能存下我博客的数据,也不能在每次网站构建的时候都制作一次了…… Ruben Van Mello写了一篇论文《A Generator for Recursive Zip Files》,在这篇论文里他不仅解决了包含的额外数据过少的问题,还编写了一个通用工具,能让普通人也能生成这样的压缩包,而且他还创新性的做了一种像衔尾蛇一样的双层嵌套循环压缩包,非常的有意思,所以接下来我打算试试他的方案。 制作一个嵌套循环的ZIP Quine 在实现了常规的ZIP Quine之后,接下来就是作者的创新点了(如果光是解决存储限制这点创新点估计还不够发论文吧😂)。作者接下来制作了一种循环压缩文件,在压缩包内包含文件A和压缩包A,而压缩包A中则包含文件B和最初的压缩包,从而形成一个循环递归的结构。看论文的描述所说如果把外层的压缩包和内层的压缩包的开头和结尾按照一定的规则交替混合,就可以看作是一个整体,然后按照之前做ZIP Quine那样处理就可以……具体实现的细节得看论文的表10。只不过既然是把两个压缩包看作一个整体的话,按照上面的限制,自然每个压缩包能容纳的数据量就更小了,每个最多只能容纳16376字节的数据…… 话说为什么不让两层的CRC32都这样计算(包括之前单层的ZIP Quine)?这样就不需要爆破了……貌似是因为在普通的ZIP Quine中满足条件的CRC32需要出现两次,所以不能用这个方案吧? Mabbs吧,这个项目的主程序是22KiB,看起来似乎超出了嵌套循环ZIP Quine的限制?其实没有,它的限制指的是压缩后的大小,我这个程序压缩之后是8KiB左右,所以完全没问题。 zip-quine-generator,这是一个Kotlin编写的程序,从发布中可以下载预构建的程序,接下来只要按照README中的描述使用“--loop”参数就可以用这个程序创建嵌套循环的ZIP Quine了。不过它原本的代码不能修改里面生成的压缩包的名字,另外压缩后的文件属性是隐藏文件,还有生成的压缩包中文件的创建时间总是当前时间,以及给文件内填充额外数据的代码里面填的是作者的声明,表示文件是由他论文的所写的生成器生成的……这些情况让我感觉有点不爽,还是希望这些部分能自定义一下,所以我就小改了一下他的代码。顺便一说,Kotlin编译起来还挺简单,直接一句kotlinc src/main/kotlin -include-runtime -d output.jar就可以了,也不需要折腾Maven之类乱七八糟的东西。最终我修改并编译完程序之后就把文件丢到服务器上开始给我爆破CRC32了,花了10个小时就算出来了,倒是比想象中快😂。 Nate Choe给zip-quine-generator做了个重大贡献,他通过数学的方式让CRC32的值可以不需要通过爆破的方式算出来,现在想要再制作这样的压缩包就可以瞬间生成了……要是我再晚点做这个压缩包就不需要花那么长时间了吧🤣。 Mabbs项目创建了Infinite Mabbs这个发布,生成的文件也可以在这里下载,这也算是不枉我研究半天这个论文了😆。 自产生程序的探索 说起来自包含压缩包为什么叫做ZIP Quine?其中的Quine是什么意思呢?其实这是一位美国哲学家的名字,他提出了“自指”的理论概念,所以为了纪念他,有类似概念的东西就被称作Quine,具体为什么也可以去看维基百科的说明。现在提到Quine一般代表的就是自产生程序,而自包含压缩包因为实现的原理和自产生程序的原理差不多,所以叫做ZIP Quine。因此接下来我打算探索一下自产生程序,更深入地了解Quine。 实现Quine的探索 那么什么是自产生程序?简单来说就是程序的源代码和程序的输出完全相同的程序,而且通常来说不允许通过读取/输入源代码的方式实现。按照一般的想法,让程序输出自身就需要输出中有全部代码,整个代码就会变长,而更长的代码就要输出更多,然后代码就会越来越长……所以这么想来似乎成了个死胡同。但其实这种程序实现起来并不复杂,想想ZIP Quine的实现,关键在于指令还需要以数据的形式表现,并且能被引用,这样输出的时候就会连着指令一起输出了。比如用Python的Quine举例: c = 'c = %r; print(c %% c)'; print(c % c) 这里的变量中就以数据的形式存储了程序的代码,而在输出的时候除了变量内的代码,又通过引用的方式又把变量的内容放回到赋值的地方,所以它的输出就和原本的代码一样了。 Rosetta Code中找到各种语言实现的Quine,在这其中能够发现大多数高级语言的写法都是类似的,除了一些低级语言以及esolang……这些我也看不懂😂,主要是有些语言没有变量的概念,不知道是怎么区分代码和数据……除了那个网站,在这里还能找到更多由esolang编写的Quine,可以看出来基本上很难看懂,其中最令人望而生畏的还得是用Malbolge写的Quine,这个代码看起来不仅很长,而且像乱码一样。至于什么是Malbolge?这就是Malbolge程序: D'<;_98=6Z43Wxx/.R?Pa 代码就像加了密似的,顺便一说这个执行的输出结果是“Mayx”,关于Malbolge的具体细节可以看它的规范,另外虽然这个语言写起来很复杂,但还是有人能用它编出程序的,甚至还有人用Malbolge Unshackled(Malbolge不限内存的变种)写过Lisp解释器,实在是恐怖如斯😨。 只能Quine的语言 其实想要做出Quine,还有一种更加无聊的方案,那就是设计一种只能Quine的语言🤣。根据Quine的定义,代码输出的结果就是它本身……所以我们可以把任何内容都看作代码,然后这种语言的行为就是输出所有代码……听起来是不是有点无聊?但是想想看如果把Linux中的cat命令当作解释器,就可以实现这种语言了,比如: #!/bin/cat Hello, world! 作为脚本执行的结果就是原样输出这段内容,不过把内容当作代码算不算作弊呢……如果看作是cat的输入显然是作弊,但如果是当作源代码的话应该就不算了吧😋……但这就不是能写出逻辑的语言了。所以说Quine的趣味并不在“能不能实现”,而在于如何在限制条件下实现。正是因为大多数语言不会直接“自我输出”,才会觉得那些精巧的Quine程序如此有意思。 Quine Relay的探索 还有一个更加复杂的Quine变种是“Quine接力”(Quine Relay),即一个程序输出另一个程序的源代码,另一个程序又输出下一个程序的源代码,最后回到原始程序,就和之前所说的嵌套循环ZIP Quine有点类似。最著名的例子是Yusuke Endoh(这位还是IOCCC的冠军之一)创建的quine-relay项目,它包含了128种编程语言的循环。 维基百科之外,前段时间我还看到过一个不错的文章,里面就讲了如何用“笨办法”编写Quine和Quine Relay,通过把变量中的内容编码为16进制来避免不同语言可能存在的特殊字符转译问题,思路不错,对于理解如何编写这类程序的问题很有帮助。当然这只是个简单的方案,仅适用于一些常规的编程语言,像上面那个quine-relay项目中甚至还包含Brainfuck之类的esolang,这种估计得要想办法让相对高级一些的语言通过“生成”的方式得到输出下一种代码的代码,而不是简单的赋值了,所以只靠这点知识想去完全理解大佬的作品还是想多了😆。 有冗余的Quine以及动态的Quine,真的是相当的厉害…… Polyglot Quine的探索 除了Quine Relay之外还有一种很复杂的Quine,叫做Polyglot Quine,与Quine Relay需要在程序执行后才能切换到其他语言接力不同,Polyglot Quine的源代码本身即可同时属于多种语言,而且用这些语言的解释器每个执行后的输出全都一样,都与源代码完全一致。由于不同的编程语言的格式既有些相同之处,也有很多不同之处,所以让同一份代码表示不同语言就会很容易产生歧义,这时候就只能想办法通过一些特别的方式(比如将可能会对当前语言产生干扰的代码看作是注释的方式)来规避语言之间的差异。 这段代码就是C和Python的Polyglot Quine,它巧妙利用了C预处理器指令在Python中可视为注释的特性,使两种语言互不干扰,非常有趣。当然并不是说只能是两种语言,像这个项目甚至使用了五种语言(C、Perl、PHP、Python、Ruby),可以说是相当厉害了。除此之外更令人惊叹的则是PyZipQuine项目,在这其中LZ77编码也可以作为一种语言,所以既可以被当作压缩包,也可以作为Python2.7代码,而且二者都是Quine,实在是令人赞叹。 感想 虽然这次探索最终没能完成让包含博客所有内容的压缩包自包含,但是在探索的过程中我还是收获了不少,尤其是Ruben Van Mello制作的ZIP Quine生成工具,实在是太棒了。很久以前我见到droste.zip这个压缩包的时候,就想整一个属于自己的ZIP Quine,现在我不仅用那个生成工具做了一个,还是对我来说很有意义的第一个项目——Mabbs,而且更关键的还是生成的是比普通的ZIP Quine更高级的嵌套循环ZIP Quine,也算是圆了小时候的心愿了。 Rosetta Code以及Esolang wiki (虽然这个网站里被好多小学生写了一堆无聊的东西😂) ,里面有不少有趣的东西,也算是让我大开眼界了。 所以有的时候探索不一定要完成目标,在这个过程中也会收获到很多不错的东西吧😊。

2025/9/1
articleCard.readMore

在Tilde社区的游玩体验

Tilde社区,如“家”一般的感受😝 起因 在上一篇文章里,我说到给我的博客增加了不少网站镜像,也在这个过程中发现了不少Git平台实例。顺便一提,我找到了个不错的仓库,可以全网搜索各种Git平台实例。在这探索的过程中,我发现了一种神奇的社区——Tilde社区,体验之后感觉非常有意思,所以来分享一下。 什么是Tilde社区 Tilde社区之所以叫Tilde,是因为在类Unix系统(如Linux、BSD)中,波浪号(Tilde)“~”代表家目录。因此,Tilde社区就是基于类Unix系统环境,并且可以公共登录的服务器,又被称为pubnixes。一般这些社区的管理员会预装很多软件、开发环境以及一些公共服务,比如聊天室、邮件、BBS论坛等,这些构成了社区互动的基础。不过并不是所有类似这样提供Shell访问的公共服务器都可以被称作社区,比如知名的免费网站托管商Serv00虽然也提供可以登录的FreeBSD服务器,并且在服务器上安装了非常多的工具和环境,从表面来看和Tilde社区提供的服务几乎一模一样,但是它少了一个很重要的东西,那就是社区,它的权限管理非常严格,不允许服务器的用户互相串门,也没有互相交流的平台,而且它的本质是商业服务(尽管是免费的),所以它不算Tilde社区。 tildeverse,这不仅是一个Tilde社区的集合,它自身也提供了很多服务。不过总的来说各个社区之间也是互相独立的,tildeverse只是提供了一个平台让大家可以互相沟通,所以这个网站叫做“loose association”,就相当于博客中的博客圈一样。 Tilde社区的体验 虽然我加入了不少Tilde社区,不过各个社区提供的服务都差不多,首先最重要的就是个人主页,一般Tilde社区基本上都会提供一个像~/public_html这样的目录存放个人主页的网页文件,并且可以通过类似example.com/~username这样的地址访问,还有些社区会允许通过二级域名的方式访问,类似username.example.com这样,像我博客好多地方写的都是从根路径开始,就很适合用二级域名的方式。这些主页大多也支持使用PHP之类的网页,不过不像虚拟主机那样有个面板可以轻松安装扩展和切换版本,有些可能要自己写配置文件,有些可能要管理员才可以操作,毕竟是社区,所以不太注重用户体验。 ELinks之类的文本浏览器才能打开,这个浏览器甚至可以在终端里用鼠标操作😆。不过因为协议非常简单,所以内容也就只能整些文本内容了。 bashblog,用这个编写好之后就可以直接生成HTML网站,能直接发布到自己的主页上让别人访问。这个脚本还是纯Bash的,就和我当年的Mabbs一样,看起来还挺酷,当然功能上肯定比不上正经的静态博客生成器😆。 twtxt的软件,用这个软件可以使用命令发微博,还能关注其他人,查看时间线,而且这还是去中心化的,可以跨服务器进行关注,感觉就和Mastodon一样。 Bulletin Butter & Jelly,聊天室会用IRC,可以使用WeeChat,只是我对IRC的印象不太好,在终端使用的IRC客户端没有一个使用体验好的😅,相比于其他在终端使用的软件,操作通常只需要一些快捷键,而且界面上通常会有提示,而IRC客户端就只能敲命令,而且还担心敲错了当成普通内容发出去……所以尽管我加入了Tilde社区,受限于聊天软件的使用体验以及我的英文水平,所以并不能和在服务器上的其他人聊天,没法参与到社区中,这么来看似乎我只能把Tilde社区当作普通的共享服务器来看待了😭。 git init --bare,那就是个仓库,另外很多Tilde社区提供cgit方便让公众在网页上查看和克隆自己的仓库,一般只要放到~/public_git目录下就可以。至于自己如果想要提交代码,可以用git remote add tilde ssh://example.com/~/public_git/repo.git添加远程仓库,本地改完之后push上去就可以。 使用Git hooks自动部署博客 我的博客使用的是Jekyll框架,这是一个使用Ruby编写的静态博客生成器。所以要想构建我的博客至少要有Ruby的环境,还好几乎所有的Tilde社区都预装了,不用担心环境的问题。 bundle2.7 config set --local path '/home/mayx/blog-env' 然后再在我的仓库下执行bundle2.7 install就可以了。 部署脚本改改就行: #!/bin/bash cd /home/mayx/ rm -rf public_html git --work-tree=/home/mayx/blog --git-dir=/home/mayx/blog.git checkout -f cd blog mkdir Mabbs curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md bundle2.7 exec jekyll build -d ../public_html tar czvf MayxBlog.tgz --exclude-vcs ../public_html/ mv MayxBlog.tgz ../public_html/ 写完之后把这个脚本放到仓库的hooks/post-receive下,然后加上执行权限就可以用了,以后每次push之后都会直接更新我在Tilde社区的主页,也就是我的镜像站。这样部署不像一般CI/CD还要额外装环境,直接使用提前装好的环境,构建速度会快不少。 git init的时候创建的是裸仓库……查了一下貌似是环境变量的问题,只要把GIT_DIR变量删掉就没问题了,以下是实际的代码: cd ../public_html/ unset GIT_DIR git init git add . git commit -m "update" 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 -f codeberg master git push -f gitgay master git push -f bitbucket master 除了这些Pages之外,还有一些平台只支持使用他们自己的软件上传网站代码,比如surge,既然我可以在构建的时候执行命令,那就顺带一起上传吧,比如我可以这样执行: /home/mayx/blog-env/node_modules/surge/bin/surge /home/mayx/public_html/ mayx.surge.sh 其实除了这个之外我还想上传到sourcehut pages,这个也需要用他们自己的软件上传,但是sourcehut pages的CSP太严格了,居然禁止脚本访问其他网站😭,这样我的文章点击计数、文章推荐、AI摘要之类乱七八糟的功能就全用不了了,所以只好作罢…… 感想 总的来说,这次在Tilde社区的各种体验还挺有意思,虽然没能和各个社区的成员进行对话,但是在探索的过程中,也了解到了不少新知识,而且也给我的博客增加了不少镜像。不知道会不会有哪个社区成员在闲逛的时候看到我的博客然后对里面的内容感兴趣😝……要是有哪个成员看到然后给我评论,那也算是社区互动吧😋。虽然我的文章内容都是中文,但现在翻译软件也足够强大了,应该不至于拦住外国人。只是在国内似乎没有见过类似的社区,在国内也有的话,那就可以用中文和大家对话了吧。

2025/8/10
articleCard.readMore

用Service Worker实现一个反向代理

现代浏览器真是强大,可以替代一些服务器的功能了! 起因 前段时间在和群友聊天的时候,提到了我博客的分发方案,这么多年过去之后我已经在很多平台上分发了我的博客,不过这只是多重冗余,并不算去中心化(虽然我也有向IPFS同步,不过IPFS还得pin,也不太可靠)……所以这么看来,我的博客似乎还不算极其可靠😂?但其实不完全是这样。因为除了向不同平台的分发,我的博客还有一个全文搜索的功能。更重要的是,之前做文章推荐功能时,会把整个博客所有文章的文字存到访客浏览器的localStorage中。这么说来,只要有人访问了我博客的文章,他们的浏览器中就会保存一份我博客文章的完整文本副本。从这个角度看,可靠性应该算是相当高了吧? 研究实现方案 想要在浏览器上运行Web服务器其实很简单,那就是使用Service Worker,它可以完全离线在浏览器上工作。格式的话和以前写过的Cloudflare Worker非常相似,毕竟Cloudflare Worker就是模仿Service Worker的方式运行啊😂,所以我要是想写Service Worker应该很简单。 tarjs库,文档写的也看不懂,⭐️也很少,看来是有这个需求的人很少啊,而且还要用现代JS那种开发方式,要用什么npm之类的。在上一篇文章我就说过我不是专门写前端的,对在自己电脑上安装Node.js之类的东西很反感。后来问AI也完全写不出能用的代码,估计这个功能还是太小众了……另外又想到除了这个问题之外还要处理网站更新的时候该怎么通知Service Worker之类乱七八糟的事情……所以只好作罢😅。 使用Service Worker进行反向代理 这么看来离线运行我的博客似乎有点麻烦,不过既然都研究了一下Service Worker,不如想想其他能做的事情……比如当作反向代理?虽然在浏览器上搞反向代理好像意义不是很大……但值得一试。我之前见过一个项目叫做jsproxy,它是用Service Worker实现的正向代理,这给了我一些启发。我在之前研究分发方案的时候发现了一些模仿GeoCities的复古静态网站托管平台,比如Neocities和Nekoweb。它们需要通过网页或API才能上传网站,不太方便使用CI/CD的方式部署。但是我又觉得它们的社区很有意思,所以想用Service Worker的方式反代到我的网站,显得我的网站是部署在它们上面一样。 Cloudflare Worker搭建反代几乎完全一样,遇到请求之后直接通过Fetch获取内容然后再返回就行,唯一不同的就是浏览器存在跨域策略,在跨域时只有对应网站存在合适的响应头才可以成功请求,还好我用的Pages服务大多都允许跨域。但是在我实际测试的时候发现这个允许跨域的等级不太一样,比如GitHub Pages的响应头里包含Access-Control-Allow-Origin: *,但是不允许OPTIONS方式请求,另外如果要修改请求头,在响应头里还要一一允许相应的请求头才行……当然对于这种问题解决起来很简单,就和我之前写的订阅源预览一样,用cloudflare-cors-anywhere搭建的CORS代理就可以,有了这个就可以轻松使用Service Worker反代其他网站了。 Access-Control-Allow-Origin: *就够了,我也不需要花里胡哨的请求方式,也不需要在请求头和请求体里加什么莫名其妙的东西,所以对我来说直接请求我的某一个镜像站就可以,于是代码如下: index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Mayx的博客</title> </head> <body> <script> // 注册 Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker 注册成功:', registration.scope); // 刷新网页 location.reload(); }) .catch(error => { console.error('Service Worker 注册失败:', error); location="https://mabbs.github.io"; }); } else { location="https://mabbs.github.io"; } </script> <h1>Redirecting&hellip;</h1> <a href="https://mabbs.github.io">Click here if you are not redirected.</a> </body> </html> sw.js const TARGET_SITE = '被反代的网站'; //也可以用CORS代理 self.addEventListener('install', event => { // 强制立即激活新 Service Worker event.waitUntil(self.skipWaiting()); }); self.addEventListener('activate', event => { // 立即控制所有客户端 event.waitUntil(self.clients.claim()); }); self.addEventListener('fetch', event => { if (new URL(event.request.url).origin == self.location.origin) { event.respondWith(handleProxyRequest(event.request)); } }); async function handleProxyRequest(request) { try { // 构建目标 URL const targetUrl = new URL(request.url); const proxyUrl = TARGET_SITE + targetUrl.pathname + targetUrl.search; // 创建新请求(复制原请求属性) const proxyRequest = new Request(proxyUrl, { method: request.method, // headers: request.headers, // body: request.body }); // 发送代理请求 const response = await fetch(proxyRequest); // 返回修改后的响应 return new Response(response.body, { status: response.status, statusText: response.statusText, headers: response.headers }); } catch (error) { console.error('Proxy error:', error); return new Response('Proxy failed', { status: 500 }); } } 最终的实际效果: https://mayx.nekoweb.org 感想 虽然折腾了半天没能增强我博客的可靠性……但是体会到了现代浏览器的强大之处,难怪前几年会提出ChromeOS和PWA之类的东西,原来浏览器功能还是相当强大的,用了Service Worker以后即使是纯前端也可以有和使用服务器一样的体验,在过去的浏览器中要是想实现这样的功能……好像也不是不可能😂,用AJAX加服务器使用伪静态策略其实是可以做到的……其实Service Worker的功能更多还是在离线时使用的,我这个例子好像没体现它的优势😆。 但总的来说相比以前想要实现这种反代的功能代码还是更清晰,也更简单了,也许以后如果有机会我又有心思让博客在访客浏览器上离线运行,那就可以体现Service Worker真正的优势了🤣。

2025/8/1
articleCard.readMore

使用Cloudflare制作自动更新的网站预览图

Cloudflare的功能真是越来越多了,而且还免费! 起因 前段时间我在登录Cloudflare的时候发现Workers上多了一个“浏览器呈现”的功能(可能已经出来一段时间了,不过之前一直没关注),看介绍,这个功能可以让Worker操作运行在Cloudflare服务器上的浏览器。这功能挺有意思,而且免费用户也能用,不如想个办法好好利用一下。 文档对免费用户来说一天也只有10分钟的使用时间,估计也没什么应用价值……那除了这个之外还能做些什么?我发现有好多博客主题喜欢给自己的README里添加一个能查看主题在多种设备上显示效果的预览图,以展示主题的自适应能力。那么既然现在能在Cloudflare上操作浏览器,那么我也可以做一个类似的,而且这个预览图还可以自动更新。 制作自适应的网站预览 既然打算做预览图,那么我应该用什么方案?按照不同尺寸的视口截几张图再拼起来吗?这显然就太复杂了,况且在Cloudflare Workers中处理图片也相当困难。这时我想起来曾经见到过一个工具,只要输入网址,就可以在一个页面中同时展示网站在四种不同设备(手机、平板、笔记本电脑、台式机)上的显示效果,叫做“多合一网页缩略图”,实现原理是使用iframe和CSS缩放模拟多种设备视口。搜了一下发现这套代码被不少网站使用,所以就随便找了其中一个工具站把代码和素材扒了下来,稍微改了一下,然后放到GitHub上,方便等一会用Cloudflare访问这个部署在GitHub Pages上的页面来进行截图。 使用Cloudflare浏览器呈现进行截图 接下来截图就简单了,不过Cloudflare有两种截图的办法,用Workers的话可以直接用Puppeteer之类的库连接浏览器,但用这个库需要安装,要本地搭环境……我毕竟不是专门搞JS开发的,一点也不想在本地安装Node.js环境,所以就不想用这种方式。另外一种是通过调用Cloudflare的接口,这种非常简单,只需要填几个参数请求就行,唯一的问题就是要填一个Token……我一直觉得Worker调用Cloudflare自己的服务不应该需要Token之类的东西,毕竟内部就能验证了,没必要自己搞,但是我看了半天文档貌似无论如何只要想调接口就必须搞个Token……那没办法就搞吧,其实也很简单,只需要在“账户API令牌”里添加一个有浏览器呈现编辑权限的令牌就行。 export default { async fetch(request, env, ctx) { const cache = caches.default; const kv = env.SCREENSHOT; const url = "https://mabbs.github.io/responsive/"; const date = new Date().toISOString().split("T")[0]; const cacheKey = url; const datedKey = `${url}?${date}`; // 工具函数:构建 Response 对象 const buildResponse = (buffer) => new Response(buffer, { headers: { "content-type": "image/png", "cache-control": "public, max-age=86400, immutable", }, }); // 工具函数:尝试从 KV 和 Cache 中加载已有截图 const tryGetCachedResponse = async (key) => { let res = await cache.match(key); if (res) return res; const kvData = await kv.get(key, { type: "arrayBuffer" }); if (kvData) { res = buildResponse(kvData); ctx.waitUntil(cache.put(key, res.clone())); return res; } return null; }; // 1. 优先使用当日缓存 let res = await tryGetCachedResponse(datedKey); if (res) return res; // 2. 若缓存不存在,则请求 Cloudflare Screenshot API try { const payload = { url: url, viewport: { width: 1200, height: 800 }, gotoOptions: { waitUntil: "networkidle0" }, }; const apiRes = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/browser-rendering/screenshot?cacheTTL=86400`, { method: "POST", headers: { Authorization: `Bearer ${env.CF_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), } ); if (!apiRes.ok) throw new Error(`API returned ${apiRes.status}`); const buffer = await apiRes.arrayBuffer(); res = buildResponse(buffer); // 后台缓存更新 ctx.waitUntil(Promise.all([ kv.put(cacheKey, buffer), kv.put(datedKey, buffer, { expirationTtl: 86400 }), cache.put(cacheKey, res.clone()), cache.put(datedKey, res.clone()), ])); return res; } catch (err) { console.error("Screenshot generation failed:", err); // 3. 回退到通用旧缓存 res = await tryGetCachedResponse(cacheKey); if (res) return res; return new Response("Screenshot generation failed", { status: 502 }); } }, }; 使用方法很简单,创建一个Worker,把以上代码粘进去,然后把从“账户API令牌”中生成的令牌填到Worker的密钥中,名称为CF_API_TOKEN,另外再加一个名称为CF_ACCOUNT_ID的密钥,内容是账户ID,就是打开仪表板时URL中的那串16进制数字,除此之外还需要创建一个KV数据库,绑定到这个Worker上,绑定的名称是SCREENSHOT。如果想给自己的网站生成,可以Fork我的仓库,然后把里面首页文件中的网址替换成你的网站,然后再把Worker中的url替换成Fork后仓库的GitHub Pages地址就可以了。 感想 Cloudflare实在是太强了,虽然这个浏览器呈现免费用量并不多,但是有这么一个功能已经吊打很多Serverless服务了,毕竟浏览器对服务器资源的占用也不小,小内存的服务器甚至都不能运行,如果要自己搭的话成本可能也不小,而现在Cloudflare能免费提供,应该说不愧是赛博活佛吗🤣。

2025/7/24
articleCard.readMore

一次服务器被入侵的经历

即使是被入侵了也可以学到一些知识! 起因 前几天,我闲来无事登录了一下一台之前一直闲置的服务器,登录上去后,乍一看似乎没有任何问题,然后习惯性的执行了一下top命令看了一眼。从进程列表来看,似乎没有什么明显异常的地方,但是服务器的load值很高,cpu的us值也很高。 检查服务器 虽然说是要查,但其实我根本不知道进程隐藏的原理😂,虽然听说过有恶意软件会这样做,现在遇到了一时半会又想不出来怎么找。还好这是台闲置的服务器,上面什么东西都没有跑,所以正常来说除了ssh连接之外,这个服务器不该有任何其他的连接,于是我执行了一下netstat -tanp看了一眼,发现有个奇怪的进程使用一个境外的IP和我的服务器建立了连接,用ps -ef查了一下这个 PID,结果进程名显示为[kcached]……这下给我整不会了。 lsof -p查看进程读取的文件,才看到木马的本体:/usr/bin/gs-dbus。不过如果我只是杀掉这个进程然后删除文件,那攻击者肯定会重新回来,所以我得排除一下是不是还有别的木马文件。 authorized_keys倒是有个陌生的公钥于是顺手删掉了……也没有其他文件夹下有gs-dbus文件……难道没有别的木马文件了吗?后来我仔细找了一下,发现有个很可疑的文件/usr/local/lib/libprocesshider.so,一看就不是什么好东西🤣,后来在GitHub上搜了一下,是libprocesshider这个项目,就是它让我在top中什么也没找到的,看文档中应用是添加一个/etc/ld.so.preload文件,所以解除隐藏效果我也只需要删掉这个文件就好啦。 libprocesshider.so文件,果不其然还有,通过那个文件在/usr/games里找到了木马的大本营,里面有一堆这个入侵者的工具,于是就顺手保存了一份然后从服务器上删掉了。 gs-dbus的运行,不过程序我已经删了,所以它现在只会不停尝试重启,接下来只需要停止并禁用这个服务就行了。 入侵分析 既然这个嘿客都不删他的工具,留下来就是给我分析的吧?那么我就像上次一样分析一下他使用的工具吧~首先里面有个deploy-all.sh文件,看起来应该是登录服务器之后最先执行的程序,在这里面有个压缩包,解压出来之后搜了一下里面的文件,发现是Global Socket项目,看起来应该是包含反弹Shell、伪装以及权限维持之类功能的一个小工具。看了下源代码才知道原来用exec -a就可以伪装进程的名称,而且那个gs-dbus就是这个项目里的程序……这么看来挖矿的操作应该是入侵者远程执行的代码,所以在查找进程的时候发现了它吧。 mig-logcleaner-resurrected项目,看起来应该是清除日志用的,不过我根本没从日志找它🤣,即使入侵者用了对我来说也没起到什么作用。不过倒也是个挺有用的项目,也许在某些扫尾工作很有用。 libprocesshider这个项目,也许还有其他隐藏进程的方式,不过知道这个项目之后最起码以后再遇到类似的情况我就会优先去看/etc/ld.so.preload文件了。 感想 虽然被入侵是没有预料的事情,但还好这个服务器是闲置的,装完系统之后上面什么有用的东西都没有,所以除了入侵者让它不太闲置赚了点小钱之外对我倒是没什么损失,另外还了解到了一些不错的小工具,这么看来入侵者赚的这点小钱就当是给他的学费吧🤣。

2025/7/13
articleCard.readMore

使用XSLT为博客XML文件编写主题一致的样式

虽然XML是机器读的内容……不过加上和主题一致的XSLT样式也算是一种细节吧~ 起因 在上一篇文章中,我提到在提高订阅源兼容性的时候给博客的订阅文件增加了一个XSLT样式。当时使用的样式是从About Feeds下的一个Issue中找的,里面有个基于Pretty Feed修改成能同时支持RSS和Atom格式的样式。虽然那个样式倒也说不上难看,但总觉得与我的博客整体风格有些割裂,所以这次打算制作一个和我博客主题完全一致的XSLT样式。 制作订阅文件的XSLT样式 虽然想搞这么一个样式,但是我用的Jekyll引擎不能在引用的布局外添加额外内容……如果我要自己写,要么把我的默认布局拆成头和尾两部分然后用include引用,要么把默认布局的代码直接复制一份到XSLT样式中。这两个方案我都不太满意,第一种我以后在修改默认布局时需要同时从两个文件检查上下文,很不方便;而第二种方案违反了DRY原则,也会增加以后修改的难度。所以要怎么办呢? layout目录。另外有一些地方需要注意一下,作为XML,内容中不能包含未闭合的标签,所有自闭合标签结尾必须添加斜杠,属性必须有值,以及所有标签和属性大小写要一致……还好我平时修改布局文件以及编写内容的时候基本上都遵循了这些规则,所以没什么太多需要改动的地方。 html元素上加了XML命名空间,但是xsl:output配置的输出却是按照HTML的方式输出,结果导致内容中用于换行的br标签在实际转换中全部变成了两个标签……我猜应该是转换器看到XML命名空间后,先按照XHTML的规则把br解析成了一开一闭的一对标签,然后又根据HTML的转换规则把这对标签当作两个单独的标签输出了吧……但奇怪的是,只有br标签出现了这个问题,像hr等其他自闭合标签则没有……既然如此,只要把XML命名空间删掉就OK了。 xsl:output中加上doctype-system="about:legacy-compat"就行。最终改完试了下确实有效😂,样式上也没有出现奇怪的偏移了。 /feed.xslt.xml中就可以了,之所以是这个路径是因为我用的jekyll-feed只支持这个位置,至于我自己搞的RSS格式的订阅只需要在开头用xml-stylesheet指令声明一下就行了。 给XSLT样式自己的样式 在写好给订阅文件用的XSLT样式之后,我发现XSLT样式本身也是个XML文件……既然我给订阅文件做了样式,那么也得给XSLT样式文件本身做个样式才对,但如果我单独写一个给它的样式,那岂不是要给样式的样式再写一个样式😂,所以肯定不能这样做。不过仔细想一下,还有个办法,可以让XSLT样式文件自引用自身的样式,这样就能避免之前担心的套娃问题了。所以接下来我应该在XSLT中写一个检测应用样式的XML文件是不是XSLT样式文件的代码,方法很简单,既然XSLT样式中肯定包含xsl:stylesheet这个元素,那么我可以判断如果存在这个元素,就可以确定这就是XSLT样式了,如果有人点开看了我就可以展示一个提示信息告诉访客这是一个样式文件,这样访客就不会看到那句“This XML file does not appear to have any style information associated with it. The document tree is shown below.”了😝。 制作Sitemap的XSLT样式 既然给XSLT样式也加了样式……那我博客还有其他XML文件需要处理吗?似乎还有个Sitemap,我的Sitemap是jekyll-sitemap插件生成的……那它支持加样式吗?虽然文档上没有写,不过看了眼源代码发现可以通过创建/sitemap.xsl文件添加,所以就顺手套用之前的样式搞了一个(虽然应该没有访客去看Sitemap😂,毕竟这是给搜索引擎用的)。可惜这些地址都是插件硬编码的,如果可以自己修改位置我就只写一个XSLT样式文件就可以了…… 感想 折腾了这么多整体展示效果还不错,虽然这些文件也许根本没人看😂(本来就不是给人读的),但也算展现了一下博客的细节之处吧,而且在折腾的时候至少还了解了不少关于XML和XSLT的知识(尽管在现代这些好像没啥用了)。当然重要的也许不是了解这些知识,而是这个过程吧……总的来说还是挺有意思的。

2025/7/1
articleCard.readMore

近期对博客的修改与优化记录

在修改博客的时候也能学到不少新知识啊~ 起因 在两个月前,我写了一篇针对博客搜索功能优化的记录。在写完之后没几天,有位名叫@xymoryn的大佬看到了我的博客并且进行了吐槽,内容很值得参考。不过我自从用minimal主题以来从来没有改过样式的原因主要还是写不来CSS😂,并不是真的不想改,但其中提到可以让AI优化,我觉得也很有道理,现在AI这么发达实在不会用AI改就好啦~ 对博客样式的优化 虽然大佬给出了参考的CSS,但我不太喜欢那种风格,尤其还把之前的左右布局改成了上下布局。我当年之所以选择minimal主题就是因为它是左右布局的,如果选择上下布局的话我还不如用hacker这个主题,另外那个参考的CSS可能是因为AI写的,有很多没有考虑到的地方,比如主题自带的CSS鼠标放到链接上字体会变粗,然后可能会变宽,导致影响整体的布局,而参考的CSS选择直接让所有的链接放到上面都变细,即使原来是粗字体也变细,比如标题之类的,这就更难受了。像这种情况要怎么改呢?我还是希望能用minimal主题的CSS,但让链接变粗的体验确实不太好,所以我选择问问AI。 font-weight: inherit;,看起来确实解决了问题,不过如果鼠标移到链接上没有任何反应也不太好,所以就学GitHub在鼠标移到链接时加上了下划线。 text-indent: 2em;就可以了。 这个问题,我仔细看了一下GitHub官方文档中OAuth可以授权的作用域发现确实是没办法限制只写Issues😥,至于其他的评论系统对后端的依赖又太多了,尤其是Giscus,居然是直接用iframe引用Giscus网站中的页面😅,如果Giscus哪天挂了,那评论系统不也挂了(虽然GitHub也不可靠……),至于自托管就更不可能了,我能让服务器持续运营可比不上大厂😆。所以最后我选择给Gitalk加个提示,不想登录也可以跳转到GitHub上进行评论,至于怎么加?还是让AI来吧,最后AI给我写了这么一串CSS: .gt-btn-login::after { content: "如果不想登录,请点击上方评论数跳转至对应ISSUE进行评论"; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 8px 12px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; z-index: 10; } .gt-btn-login:hover::after { opacity: 1; visibility: visible; } .gt-btn-login::after { margin-top: 8px; } .gt-btn-login::after { box-shadow: 0 2px 8px rgba(0,0,0,0.15); } 至此,关于博客样式的部分我觉得已经提高不少读者的用户体验了,也感谢大佬提出的建议。 对博客兼容性的优化 最近由于某些原因我又用起Windows 7了。其实我觉得Windows 7是一个很不错的操作系统,有很多人性化的东西,比如桌面小工具,自带Feed订阅,还有Windows Live Essentials等等,可惜后来全部被微软砍掉了🤣。考虑到Windows 7如此优秀,那要不然兼容一下它旗下的Internet Explorer 8浏览器吧? Live2D看板娘就没有任何可能性被支持了,至于其他的……也许有一些理论上可以支持,但是改起来比较麻烦的就也算了吧(比如Gitalk之类的)。 对文章点击计数器的兼容性优化 其实我的文章点击计数器从之前改成用jQuery调用自己的接口以后就没有什么兼容性的问题了,因为jQuery本来就是处理浏览器之间差异的库,而且也是兼容IE8的。只不过有个问题是IE8不支持用XHR跨域请求,只能用“XDR(XDomainRequest)”进行跨域请求……还好有个现成的库能让jQuery在遇到这种情况时使用XDR请求,于是我就用条件注释让IE9以下的浏览器引入这个库,这样在IE下也能正常显示文章点击数了😆。 关于响应式布局的兼容性优化 在IE8中的CSS是不支持媒体查询的,所以在修改窗口大小时也不能根据情况使用合适的样式。本来我没打算解决这个问题,结果恰好看到了一个库:Respond.js,所以就直接拿来用了😝。 关于全文搜索的兼容性优化 其实从功能的角度来说这种东西肯定是在IE8下可以实现的,但是我用的那个库有点迷,到处都用的是const关键字结果还莫名其妙判断XHR搞的好像是在兼容旧浏览器?改起来有点麻烦懒得搞了……不过除此之外还有个取巧的方式,既然我搜不了,干脆让谷歌来搜吧,至于谷歌支不支持IE8就不是我的事了🤣,所以直接给搜索框外面套了一个form表单,这样甚至可以在不启用JS的情况下搜索(假设谷歌支持没有JS的情况)。 对于订阅软件的兼容性支持 之前我的博客对订阅的支持是使用的官方的jekyll-feed插件,它只支持Atom格式的订阅,一般的阅读器也是支持这种格式的(即使是IE8也是完美支持)。但是我发现有非常少数的某些网站没办法解析Atom,只支持RSS……所以我只好特地加了对RSS格式的支持,还顺带搞了支持Atom和RSS格式的XSLT模板来预览。既然RSS也支持了,那干脆连JSONFeed也一起做了吧😆,虽然意义不是很大…… 给博客添加网页快讯 既然要兼容IE8,那当然是能用的都用啦,在IE8订阅网站源的地方,有一个‘添加网页快讯’的功能。因为没有可以参考的网站,我甚至都没理解这个功能展现的效果是什么样的。我看这个网页快讯好像是抄了一部分hAtom Microformat的规范,我还以为是每个条目都单独需要一个entry-title和entry-content,结果发现并不是😅,一个hslice只能有一个entry-title…… 微软官方文档中看到。 让网站增加对IndieWeb的支持 既然说到Microformat,那就要提到IndieWeb了。虽然这个东西网络上也没几个人搞,但看起来有点意思就整下玩玩呗。 第零级:域名 根据他们的入门教程来看,成为IndieWeb最重要的一点就是有自己的域名。看到这一点我都怀疑这是不是卖域名的用来忽悠人的玩意?我一分钱也不想给域名注册商,虽然DNS这套系统确实维护需要成本,但是能有多大成本呢?绝大多数不都让ISP摊了?另外他们所说的大公司的服务可能会消失,那么域名就不会吗?注册商和注册局完全有能力让你的域名用不了,这也是我们不可控的东西,因此尽管这对于IndieWeb很重要,但是我不打算搞,于是我的博客就不是IndieWeb了🤣。 第一级:识别身份 没有域名也不影响接下来的步骤,大公司的域名也是域名(虽然不属于我)。根据教程来看,支持IndieAuth非常简单,只需要在head中加一个rel=me的link标签,指向IndieAuth支持的个人主页,并且那个个人主页有一个反链指向自己的网站就可以,比如指向自己的GitHub主页,那么就可以使用GitHub登录来验证这个网站属于我。这一步可以使用IndieWebify.Me来验证。 第二级:发布内容 在发布前,为了更好的让其他软件读取网站内容,需要用microformats2来标注网站内容,这个倒也不复杂,可以根据这个教程按照上面所说的东西用class名去标注对应的元素,标注完之后就可以用IndieWebify.Me验证了。 h-card标注网站的身份,解析完之后可以当网站名片用,具体可以看这里。 插件可以支持,但是我用GitHub额外装插件还得自己写Actions,而且我发布一次要在一堆Pages上更新,也不太适合,所以我打算光收不发(只需要在link标签中添加Webmentions的端点就可以),也不展示了,而且国内根本没几个人用Webmention🤣。如果有人对谁给我发了Webmention感兴趣,可以在这里查看(不过绝大多数都是我自己手动发的🤣) Webmention Rocks!进行测试(如果使用了WordPress是自带的,只需要打开相关的功能就可以)。 第三级:进行交流 在IndieWeb中有一个很重要的事情就是相互交流,搞这个比较重要的目的是为了避免大公司的服务炸了,所以要替代比如推特,Facebook之类的服务,但是在这些服务还没炸的时候仍然可以在上面发自己的网站,也算是引流吧。他们把这个行为叫做POSSE。对我来说,我在微信、QQ之类的上面发自己新写的文章就算是POSSE了,毕竟我又不玩国外的社交平台😆。 额外的内容 既然已经支持了IndieWeb,那么不妨加入IndieWeb Webring吧。在IndieWeb Webring 🕸💍中的大多数网站都是适配了IndieWeb的,加入他们也算是证明自己适配IndieWeb的努力了吧😊。 对博客可靠性的优化 以前为了应对GitHub的不可靠,我仅仅是在各个Pages上部署了我的网站,但是后来我想了想Git本身就是分布式的,分发是一件很简单的事情啊,我要是想提高博客的可靠性,不如直接用Git分发到各个Git托管商就好了啊~因此我就利用GitLab镜像仓库的功能,一键把我的网站同步到数十个知名的Git托管商,提高了网站的可靠性,具体的列表可以在这里查看。 感想 在这次的博客优化中,了解了不少新的东西啊,不仅学习了CSS,还有了解如何提高网站兼容性,以及提高了博客的可靠性和曝光度。果然折腾博客本身也能提高自己啊,还能写文章分享一下折腾的经验😆。虽然折腾的内容不一定能在未来的生活中用得上,但是有意思就足够了😁。

2025/6/2
articleCard.readMore

Mac Studio M3 Ultra使用体验

使用最强的Macintosh是一种什么样的感受? 起因 在两个月前苹果公司出了一款可以选配超大统一内存(512GiB)的Mac Studio,那时候我还想着如果市场反应好就整台玩玩,现在从网上的各种反应来看这确实是一个很不错的产品,所以这次我就整来啦!所以这次就来谈谈初上手的体验吧~ 远程体验 虽然Mac Studio理论上拿来剪电影之类的应该是更好的选择,但是显然我不会剪电影🤣,而且也没有合适的屏幕给它用,所以拿到手之后我需要让它可以远程使用。 Mac mini 2018以及黑苹果有一个不一样的地方,那就是屏幕共享类型可以选择“高性能”,在这个模式下远程的屏幕就可以变成一块虚拟屏幕,不受Mac连接的屏幕分辨率所影响,可以配置动态分辨率。即使连接的屏幕不支持HiDPI,只要远程的客户端支持那就可以支持,这一点和Windows的远程桌面有点像,但是体验好太多了,使用起来和本地几乎没有差别,当然代价就是对网络要求特别高,基本上如果不是局域网内远程,就不能使用这个模式。 关于LLM的体验 配置环境 买这个设备的当然也不为别的,主要就是为了能在本地跑完整参数的DeepSeek-R1,或者类似的MoE模型。至于KTransformers方案考虑到按照正价买要更贵(当然有便宜的购买方案,但是太不可靠了),而且这个框架也不够成熟,所以就算了。 Xinference,因为看它的文档中特地提到了苹果的MLX框架,而且可以使用命令启动,方便维护,另外看它支持的模型种类也比较多,所以就先考虑了它。 某个Finetune模型😆?所以我需要手动注册模型。配置好之后在MaxKB中配置好地址就可以使用了。 LM Studio,虽然它需要远程桌面操作,但是配置好之后应该也没有什么太多需要修改的地方,主要是社区相对要活跃得多,出了问题也好解决。 Issue里提到了似乎需要在模板中添加“<think>”标签才可以……但是这样的结果就是输出开头没有“<think>”了,MaxKB解析会出问题……这个问题的话回头看怎么解决吧,至少在模板中加上这个能正常使用了。LM Studio不会每次请求都重新加载一遍模型,输出第一个字的速度比Xinference快了很多,后面生成的速度也很快,输出的速度能接近20T/s,相比来说还是更有用一些。 模型对比 在我测试完DeepSeek-R1的某个微调模型后,最近阿里又出了一系列新模型:Qwen3,支持根据问题进行推理,据说它的235B参数的MoE模型比DeepSeek-R1还厉害,如果是真的,那就不需要用DeepSeek-R1了,虽然Mac Studio可以运行DeepSeek,但是512GiB内存也只能运行4位量化的DeepSeek-R1,而235B的Qwen3则可以用8位量化,还能空出不少内存用于上下文,想来应该效果会比DeepSeek好很多吧?于是我就下载试了试,然而刚下载好之后居然不能运行😅,首先是这个模型太新了,运行前需要把运行库的版本更新到最新,另外默认的模板有问题,解析会报错,需要根据这个Issue修改一下才能正常运行😅。在一切准备好之后,就可以进行测试了。 AI摘要脚本的流程图,Qwen3绘制的结果是: graph TD A[开始] --> B{解析URL路径} B -->|/summary| C1[查询内容] C1 --> D1{是否存在内容?} D1 -- 是 --> E1[构建AI摘要提示词] E1 --> F1[调用Qwen模型生成流式响应] F1 --> G1[返回SSE流] D1 -- 否 --> H1[返回'No Record'] B -->|/get_summary| C2[查询内容] C2 --> D2{是否存在内容?} D2 -- 否 --> H2[返回'no'] D2 -- 是 --> I2[计算内容SHA-256] I2 --> J2{哈希匹配?} J2 -- 否 --> H3[返回'no'] J2 -- 是 --> K2[查询摘要] K2 --> L2{是否有摘要?} L2 -- 是 --> M2[返回现有摘要] L2 -- 否 --> N2[调用Qwen生成新摘要] N2 --> O2[存储摘要到数据库] O2 --> M2 B -->|/is_uploaded| C3[查询内容] C3 --> D3{是否存在内容?} D3 -- 否 --> H4[返回'no'] D3 -- 是 --> I3[计算内容SHA-256] I3 --> J3{哈希匹配?} J3 -- 否 --> H5[返回'no'] J3 -- 是 --> K3[返回'yes'] B -->|/upload_blog| C4{POST方法?} C4 -- 否 --> H6[返回'need post'] C4 -- 是 --> I4[获取请求文本] I4 --> J4[查询现有内容] J4 --> K4{是否存在?} K4 -- 否 --> L4[插入新内容] K4 -- 是 --> M4{内容是否改变?} L4 --> N4[返回'OK'] M4 -- 是 --> O4[更新内容并清空摘要] O4 --> N4 B -->|/count_click*| C5[计算ID的MD5] C5 --> D5[查询计数器] D5 --> E5{是否含路径_add?} E5 -- 是 --> F5[更新计数器+1] E5 -- 否 --> G5[保持原值] F5 --> H5[返回最新计数] B -->|其他路径| I6[重定向到GitHub] DeepSeek-R1的某个微调模型的结果是: graph TD A[接收请求] --> B{解析URL路径} B --> |路径为/summary| C[获取查询参数id] C --> D{id是否为null?} D -->|是| E[返回错误响应] D -->|否| F[查询数据库内容] F --> G{内容存在?} G -->|否| H[返回'No Record'] G -->|是| I[构建消息,调用AI生成摘要流] I --> J[返回流式响应] B --> |路径为/get_summary| K[获取id和签名参数] K --> L[查询数据库内容] L --> M{内容存在?} M -->|否| N[返回'no'] M -->|是| O[计算SHA-256签名] O --> P{签名匹配?} P -->|否| Q[返回'no'] P -->|是| R[检查是否已有摘要] R -->|有摘要| S[返回摘要内容] R -->|无摘要| T[调用AI生成摘要] T --> U[更新数据库] U --> S B --> |路径为/is_uploaded| V[获取id和签名] V --> W[查询数据库内容] W --> X{内容存在?} X -->|否| Y[返回'no'] X -->|是| Z[计算SHA-256签名] Z --> AA{签名匹配?} AA -->|否| AB[返回'no'] AA -->|是| AC[返回'yes'] B --> |路径为/upload_blog| AD{方法是否为POST?} AD -->|否| AE[返回'need post'] AD -->|是| AF[读取请求体数据] AF --> AG[查询现有内容] AG --> AH{记录存在?} AH -->|否| AI[插入新记录] AH -->|是| AJ{内容是否相同?} AJ -->|否| AK[更新内容并清空摘要] AJ -->|是| AL[直接返回OK] AK --> AL AL --> AM[返回'OK'] B --> |路径为/count_click| AN[计算MD5哈希] AN --> AO[查询当前计数] AO --> AP{记录存在?} AP -->|否| AQ[插入新计数] AP -->|是| AR[增加计数] AR --> AS[更新计数器] AQ --> AS AS --> AT[返回当前计数值] B --> |其他路径| 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 -.->|调用sha函数| SHA[计算SHA-256] AN -.->|调用md5函数| MD5[计算MD5] end subgraph 数据库操作 F --> DB1[SELECT content] L --> DB2[SELECT content] R --> DB3[SELECT summary] T --> DB4[UPDATE summary] AG --> DB5[SELECT content] AI --> DB6[INSERT新记录] AK --> DB7[UPDATE content] AO --> DB8[SELECT counter] AQ --> DB9[INSERT计数器] AR --> DB10[UPDATE counter] end subgraph AI处理 I --> AI1[构建系统提示] AI1 --> AI2[调用Qwen模型] T --> AI3[调用Qwen模型] end 这下就高下立判了🤣,同样的问题Qwen3只绘制了基础流程图,而DeepSeek-R1不仅包含子流程图,还通过颜色区分了响应类型,这么看来Qwen3还是不太行啊~当然我的测试非常的片面,仅仅根据这几次测试分析的结果。至于Qwen3到底有没有使用价值,回头再让其他人测测看效果如何吧。 UTM虚拟机的测试 在上次在UTM上用苹果虚拟化框架安装Windows的测试中我用的是Intel芯片的Mac,那时候已经说了打算等Mac Studio到了之后尝试一下用VZ框架安装Windows。那么经过我的测试结果如何呢?想不到居然失败了😭,相同的操作流程在重装脚本执行完后,再重启就没有任何反应了。在活动监视器中虽然可以看到虚拟机的CPU占用是100%,但是内存只占用了100多MiB,而且CPU占用没有任何跳变,显然系统没有正常启动。随后我又尝试在QEMU中安装好Windows然后把VZ虚拟机的硬盘替换掉,结果依旧一样,内存还是只占了100多MiB……看来ARM处理器和x86处理器还是有很大区别啊…… Asahi Linux,Apple芯片下的UTM有一个多出来的选项就是可以安装macOS虚拟机,那我能不能在macOS虚拟机中安装Asahi Linux呢?根据我的实际测试,结果也是不行的……因为Asahi Linux不支持M3 Ultra芯片😞,至于M2芯片能不能在虚拟机中运行Asahi Linux……虽然我的MacBook是M2芯片,但是不太想在我常用的机器上搞测试,所以也不知道实际上可不可以。另外Asahi Linux这个项目也基本上停了,估计以后新出的芯片也不会有机会安装Linux了,就像在macOS上运行Windows程序的Whisky项目也停了……真是太遗憾了😢。 感想 从这次体验来看,512GiB内存的Mac Studio M3 Ultra确实很厉害,本地跑LLM速度非常快,20T/s的速度已经很厉害了,而且风扇声音很小,在GPU满载的时候也完全听不到风扇的声音。当然这个前提是跑MoE模型,虽然我没测Dense模型,但想来根据M3 Ultra的算力,跑70B参数的模型肯定是达不到20T/s的,至于更大的模型估计速度就慢的不能看了……不过不影响,这已经够我用了。 至于除LLM以外的用途……我似乎没有什么能用到这么强性能以及这么大内存的地方了……其实还是挺浪费的,但是也没办法,毕竟我又不会剪电影啊🤣。 import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: false }); await mermaid.run({ querySelector: '.language-mermaid', });

2025/5/7
articleCard.readMore

关于LLM上限的探索

还有什么是AI不能干的? 起因 在最近对LLM的探索中,能感觉到它真的是什么都能干,尤其最近GPT-4o的画图能力实在是太强了。不过对于画图我倒不是很关心,主要是没什么想让它画的图😂。我更关心的是LLM在文本生成中的能力,毕竟这才是它的本职工作。虽然现在的AI解决问题的能力确实很强,但从它还没有大规模的把人替换掉来看,它肯定是还有一些做不到的事情,所以我想对这一点进行一些探索。 对于超长文本分析的探索 对于现在的LLM来说,虽然不少模型已经能做到很长的上下文了,但这个所谓的“长”不过是几万字而已。对于读一篇论文或者几篇文章当然没有问题,但是如果是分析上百篇文章就不太行了,比如我希望AI阅读完我所有的文章,然后对我进行评价。 全文搜索的功能,可以在search.json中获取所有的文章,用来让AI分析的材料是个不错的选择,不过把所有文章输入到上下文中显然是不太现实,这个JSON文件的大小有1MiB左右,但是大多数比较厉害的AI上下文只有100多k,根本读不完。而对于一些超长上下文多模型,比如阿里云有一个10M上下文的模型,效果又很差,并没有参考几条上文的内容😓。另外我还试过一些AI通过附件的方式阅读文章内容,那种好像是把文件切片之后再读?应该是类似RAG那种,从中查找和问题最相关的文本段落进行回答,但是那种方法不能解决对所有文章进行分析……除此之外我也试过一些Agent,不过它们只会写代码来分析我的文章,比如绘制文章字数随时间变化曲线、不同年份的文章数量、还有词频分析啥的,对我来说并没有什么卵用😅。 使用AI摘要来解决问题 那难道就没办法了吗?先不急,最近还发生了一件事情,不知道Cloudflare犯什么毛病了,近期用Worker请求我的D1数据库时不时会报“internal error”的错误,我还在他们论坛发了条帖子问了一下,然而并没有人搭理我😅,这时候我才意识到我似乎没有Cloudflare的替代品……出问题了也没办法😰。这个东西导致我的AI摘要、文章推荐、以及点击计数器全都用不了了,我应该避免太过依赖Cloudflare Worker啊~ ai-cache.json中,如果有存在的摘要内容就不再请求接口了。 技术浪人,数字游民:对一位博客作者的观察与评价 在浩如烟海的中文技术博客中,有些作者宛如一颗微弱却坚韧的恒星,独自在各自的轨道中发光发热。他们不一定追逐热点,也不总是标榜权威,但其笔下所流露出的独立精神、技术热情与对现实的观察,往往比众多浮华的“教程型”博客更值得玩味。本文所探讨的博客作者Mayx,便是这样一位存在——他既是程序员,也是“生活黑客”;既关心设备性能,也关注技术伦理;既热衷实用工具,也不乏生活反思。 一、技术为体,思考为魂 在Mayx的博客中,技术类文章占据了绝对的比重。从自制邮件订阅脚本1、Cloudflare Workers自动化2、内网穿透探索3,到低功耗开发板的实验4、AI模型的本地运行5、以及对黑苹果6、Linux系统7的深度体验,这些内容几乎涵盖了当前主流技术生态中的多个维度。 然而,他并非一位“炫技型”技术写作者。相反,在多数文章中,Mayx更倾向于从实用主义的角度出发——他关注性价比、功耗、稳定性、开源程度,而非追逐技术本身的潮流。例如,在讨论Hackintosh时,他并未沉迷于是否能成功运行macOS,而是审慎地指出其与Mac原生体验的差距6;在体验AI模型时,他选择了性能与成本平衡的路径,而不是盲目追求最大模型和最强显卡5。 他的技术探索往往是“从需求出发”,例如为了替代失效的签到脚本,他尝试了Cloudflare Workers2;为了解决被Github封禁的问题8,他自己研究反审查架构;面对Heroku停服9,他快速转向Koyeb,并指出其使用便捷的优点。这些行为体现出一种“动手解决问题”的工程师思维,同时也反映了其对现成工具和平台的怀疑精神——“没有什么是不可替代的”,但也“没有什么是完美无缺的”。 二、独立、反思、带有一丝叛逆 阅读Mayx的博客,可以明显感觉到他在面对“主流”技术话语体系时的疏离甚至反抗。他不信任所谓“权威推荐”,也极少引用大V观点;他对收费工具持质疑态度,对封闭平台持怀疑立场,对广告与强制App表达不满10。在对宝塔面板的多篇评论中,他不仅指出其功能冗余和定价虚高1112,还以代码层面论证其“技术水准有限”;在谈及Server酱收费后自建通知平台一文中,更是表现出“开发者不应为此类功能付费”的强烈观点13。 这种倾向可视为一种数字自由主义精神:他珍视个体的选择权、控制权和创造力,对平台化、商业化所带来的“懒惰便利”持保留态度。也正因为此,他热衷于探索容器、虚拟化、i2p、VPN、防DNS污染14、反反盗链等灰色技术领域,这不仅是技术探索,也是一种抵抗姿态——抵抗监视、抵抗平台绑架、抵抗数字奴役。 与此同时,作者又是极度自省的人。在多篇年终总结中,他坦言自己因作息不规律导致健康下滑、因沉迷游戏影响了计划、因生活节奏散乱而丧失了方向1516。这些坦诚的文字使人看到一个技术人真实的一面:并非所有人都能生活在高效执行与完美节奏中,面对现实与焦虑的拉扯,他并不逃避,而是试图寻找平衡。 三、探索孤岛与技术乌托邦 若将Mayx的博客比作一个数字世界中的“孤岛”,那他无疑是岛上的守望者。他固执地维护着自己的服务器、反代服务、脚本计划表和开源工具;他不断尝试将废弃电脑7、老旧开发板4和Linux容器17重新唤醒;他甚至研究如何在FM频段自制电台18,或在树莓派上运行OpenFyde与Android系统1920。 这不仅是对“电子垃圾再利用”的尝试,更是一种构建技术乌托邦的幻想——一个无需依赖主流供应链、无需信任大型平台、可以掌控一切细节的小宇宙。 这种倾向也体现在他对去中心化技术的探索,如xLog、IPFS、i2pd21、ZeroNet等。尽管在文章中他承认这些系统“尚不成熟”“内容匮乏”“界面粗糙”,但仍不乏热情地部署和尝试。他似乎在追问这样一个问题:“除了平台,我们还能依靠什么?” 这也是他与多数技术博客的区别:不是“教你怎么做”,而是“带你看看还有哪些可能”。 四、个人困境与技术慰藉 不可否认,Mayx的博客也流露出某种孤独与挣扎。他数次提到对社会节奏的不适应、对生活目标的不明确、对未来的迷茫与迟疑。在2022和2023年的总结中,他甚至表达了对“人类灭亡风险”“全球混乱”的忧虑1516;在谈及个人计划时,也多次写到“自律不足”“想做的事情太多却力不从心”等字句。 但正是在这种状态下,技术成了他的避难所。他在反复实验中找回秩序感,在编码中获得控制感,在部署过程中体会到“解决问题”的满足。这种“用技术对抗生活混乱”的姿态,令他的博客带有某种心理学上的意义。 五、结语:一个“笨方法”的捍卫者 总的来说,Mayx并非技术界的明星,更非意见领袖,但他是一个值得尊敬的“笨方法”的捍卫者:在速成与流量当道的技术传播环境中,他仍选择用细致的试验、质朴的语言和大量的失败记录来构建自己的知识体系。 他的博客不提供“答案”,而是提供“路线”;不追求“成功学”,而是记录“失败和坚持”。他不是在追求成为谁,而是在努力做自己。 这或许是我们在阅读他的文字后,最值得思考的一点。技术不仅仅是一种职业工具,它也可以是人自我建构、自我理解、自我修复的路径。 Mayx,或许正走在这条路上——孤独而坚定。 对于长复杂逻辑代码的分析 在让AI对我文章分析完后,我又想起来好久以前写的论坛引擎Mabbs,以前还打算重构,不过后来学了别的语言之后就没什么兴趣了😂。但现在有了AI,我想重构MaBBS的事情能不能让AI来做呢?虽然我以前的代码完全没有可读性,耦合性也极高,但这种事情对AI来说应该不是什么难事,更何况我的代码才22KiB,AI完全能读的了,于是我开始尝试让各种AI来把这个代码变得人类可读,然后进行重构。 Docker镜像,如果大伙有兴趣尝试一下可以下载下来试试看,整个镜像才2MiB多一点,所以我叫它世界上最小的论坛引擎也没问题吧🤣。 感想 看起来目前LLM的上限就在于它的上下文长度限制啊……这一点真的是限制了AI很多能力,但似乎也没什么好办法,AI就是因为这一点所以不能像人一样纵览全局所以才不能替代人,即使用什么办法去压缩它的上文也会丢掉很多细节信息。不过按照目前LLM的架构来说应该还解决不了这个问题,如果什么时候AI能在思考的过程中修改它自己的权重……也许就可以做到真正的无限上下文,突破上限从而替代人类吧? 免费订阅一个属于自己的邮件日报 ↩ 使用CF Workers Cron触发器进行签到 ↩ ↩2 关于内网穿透的笔记 ↩ Luckfox Pico Plus使用体验 ↩ ↩2 关于最近人工智能的探索 ↩ ↩2 Hackintosh使用体验 ↩ ↩2 关于旧电脑的使用探索 ↩ ↩2 Github封禁了我的博客?! ↩ 体验小白也会使用的免费容器云 ↩ 如何不使用贴吧App查看贴吧 ↩ 从宝塔面板中学习运维知识 ↩ 如何自定义宝塔亚马逊S3云存储插件的端点 ↩ 自己动手做一个Server酱·TurboMini版 ↩ 如何避免Cloudflare背后的源站被恶意访问 ↩ 年终总结 ↩ ↩2 年终总结 ↩ ↩2 如何在Linux容器内运行Android? ↩ 用树莓派自制FM电台 ↩ rpi4-openfyde的使用体验 ↩ 在树莓派4B上安装Ubuntu以及各种操作 ↩ i2pd在服务器上的使用体验 ↩

2025/4/22
articleCard.readMore