<?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>2026-03-04T20:03:29+08:00</updated><id>/atom.xml</id><title type="html">Mayx的博客</title><subtitle>Mayx's Home Page</subtitle><author><name>mayx</name></author><entry><title type="html">近期LLM的部署与应用经历(3)</title><link href="/2026/03/01/llm3.html" rel="alternate" type="text/html" title="近期LLM的部署与应用经历(3)" /><published>2026-03-01T00:00:00+08:00</published><updated>2026-03-01T00:00:00+08:00</updated><id>/2026/03/01/llm3</id><content type="html" xml:base="/2026/03/01/llm3.html">&lt;p&gt;用更多的方式探索AI！&lt;!--more--&gt;&lt;/p&gt;

&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在一年前，我&lt;a href=&quot;/2025/02/22/llm.html&quot;&gt;整了张RTX4090 48GiB魔改版&lt;/a&gt;用来跑DeepSeek-R1 70B的4bit量化模型，不过都已经过了这么长时间，这个模型也已经是过时的东西了……我之前在&lt;a href=&quot;/2025/05/07/mac-studio.html&quot;&gt;Mac Studio M3 Ultra&lt;/a&gt;上试了一下OpenAI在半年前出的gpt-oss-120b模型，感觉效果还挺不错，只不过因为M3 Ultra的GPU实际性能比不上正经高端的独显，所以它在上下文很长的情况下还是有点慢，因此我又整了张RTX4090 48GiB，想整个双路试试更快的GPT-OSS模型，总共96GiB的显存应该够跑这个模型了。&lt;/p&gt;

&lt;h1 id=&quot;在两张rtx4090-48g上运行gpt-oss&quot;&gt;在两张RTX4090 48G上运行GPT-OSS&lt;/h1&gt;
&lt;p&gt;既然现在我手头有两张4090了，那继续用i5-8400处理器的主机似乎不太合适，主要是那个主板就一个PCIe插槽，想插两张显卡也做不到，那买个新的不知道买啥……不管怎么说既然用这么高级的显卡，至少得让它跑满。在两张显卡上跑模型似乎卡间的通信速度比较重要，那最起码得整个支持2个PCIe4.0 x16的板U套装才行，这种级别的没有消费级产品，只能考虑服务器或工作站了。不过我对服务器和工作站了解得并不多，所以就问了问AI哪个支持2个PCIe4.0 x16的平台最便宜，结果AI推荐了TRX40+&lt;a href=&quot;https://www.amd.com/zh-cn/support/downloads/drivers.html/processors/ryzen-threadripper/ryzen-threadripper-3000-series/amd-ryzen-threadripper-3960x.html&quot;&gt;TR 3960X&lt;/a&gt;，于是就按照AI的说法整了一套。 &lt;br /&gt;
  这套板U差不多4000CNY，价格倒是还行，如果买现役的估计主板都比显卡贵了。但后来我发现这个并不是最便宜的😂，搜了一下买寨版+&lt;a href=&quot;https://www.amd.com/zh-cn/support/downloads/drivers.html/processors/epyc/epyc-7002-series/amd-epyc-7502.html&quot;&gt;EPYC 7502&lt;/a&gt;还能再便宜1000CNY，而且通道数更多，插4张显卡都没问题……不过买都买了，就先用吧，看来AI的话不能随便信😥。 &lt;br /&gt;
  之前我跑模型为了方便，基本上都用的是&lt;a href=&quot;https://github.com/ollama/ollama&quot;&gt;Ollama&lt;/a&gt;，不过听说Ollama多卡运行的效率很低，而且多并发的效果不太好，所以这次换了新电脑之后我想试试&lt;a href=&quot;https://github.com/vllm-project/vllm&quot;&gt;vLLM&lt;/a&gt;，据说一般生产级的AI都用的是这个框架。 &lt;br /&gt;
  安装vLLM倒是比想象得简单很多，直接一句&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install vllm&lt;/code&gt;就可以了，其实并没有比Ollama复杂多少。我看了一下&lt;a href=&quot;https://developers.openai.com/cookbook/articles/gpt-oss/run-vllm/&quot;&gt;OpenAI&lt;/a&gt;和&lt;a href=&quot;https://docs.vllm.ai/projects/recipes/en/latest/OpenAI/GPT-OSS.html&quot;&gt;vLLM&lt;/a&gt;运行GPT-OSS的官方文档，发现启动也非常简单，一般来说直接执行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vllm serve openai/gpt-oss-120b&lt;/code&gt;就可以。不过直接执行是对于单卡的，我用两张卡需要加个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--tensor-parallel-size 2&lt;/code&gt;参数启用张量并行，不然会爆显存。另外考虑到这个模型本身占掉60多GiB的显存之后剩下30GiB还是看起来有点少，所以额外加了个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--kv-cache-dtype fp8&lt;/code&gt;参数降低上下文对显存的占用，毕竟模型本身也就是4bit量化的，加了这个应该不会对它的能力有什么影响。除此之外AI还给我推荐了个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--enable-chunked-prefill&lt;/code&gt;参数，说是也能避免爆显存的问题。 &lt;br /&gt;
  一切准备好之后直接执行，程序就自动开始下载模型了，过了几个小时，终于下载完成，顺便一说启动的时候还显示推荐安装&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;torch_c_dlpack_ext&lt;/code&gt;库，虽然不知道是干啥的，但也顺手安装了。启动完成之后我试了一下，效果非常好，不并发的情况下直接用能达到接近190Tps，可以说是相当快了，而且这个模型的水平也算是开源中的上游水平，应该算是又快又好吧……看来多来一张4090还是挺划算嘛。只不过这个东西基本上就我一个人用，所以也没什么能测一下并发的场景……虽然很快，但还是有点浪费性能吧。&lt;/p&gt;

&lt;h1 id=&quot;最近deepseek-1m上下文的使用体验&quot;&gt;最近DeepSeek 1M上下文的使用体验&lt;/h1&gt;
&lt;p&gt;前段时间DeepSeek又出了新的模型，最高可以支持1M长的上下文，而且听说模型规模变小了，所以速度也很快。可惜的是到目前为止还没有开放权重。当然就算开放权重了用2张4090估计也没有足够的显存分配给上下文，至于Mac Studio感觉在长上下文的情况下运行速度应该会很慢…… &lt;br /&gt;
  不过我对这个1M上下文还是挺感兴趣，因为好久之前我写过一篇&lt;a href=&quot;/2025/04/22/ai-limit.html&quot;&gt;关于LLM能力上限&lt;/a&gt;的文章，在那篇文章中其实我遇到的问题基本上也就是由上下文不足导致的。那既然现在DeepSeek支持了1M的上下文，那我就应该试试之前因为局限性而妥协的一些东西了。 &lt;br /&gt;
  这次我没有用摘要，而是直接把包含整个博客内容的&lt;a href=&quot;/search.json&quot;&gt;search.json&lt;/a&gt;文件上传到DeepSeek，然后向它问了问我的一些问题。试了一下效果非常不错，用摘要会省略的一些细节它基本上都可以展现出来，我试了试让它给我生成一份简历，它甚至在所有文章中找到了我的博客地址、GitHub和邮箱地址，之前用摘要显然是做不到这一点的，这个长上下文还是挺有用啊。 &lt;br /&gt;
  另外我还试了试让它根据文章内容分析十六型人格，并且我自己去答了一遍那个测试，结果也是相同的，说明它真的是在几秒内就读完了我的所有文章而且也完全理解了，真的是非常厉害。 &lt;br /&gt;
  只是拿AI分析我的文章也许只有我自己了😂，实际上根本没人对我感兴趣，也就只有我自己拿来给自己看……当然如果我的博客能比我活得长，不知道会不会有未来人会对我感兴趣呢……总之对于现在肯定是毫无意义了。 &lt;br /&gt;
  除了这些之外，我又试了一下让DeepSeek重构我的&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;，这次生成效果看起来很不错了，虽然代码我没细看，不确定能不能运行，但至少没有偷懒只写一点点，一口气写了80KiB多的代码，这也是长上下文带来的好处吧。总之目前这个长上下文的DeepSeek也算是突破了之前我认为的上限，看来LLM真的是前景无限啊。 &lt;br /&gt;
  另外我发现这次更新的DeepSeek居然了解我的博客，我问了一下它“你知道Mayx的博客是哪个博客吗？”，它居然知道，能说出域名，而且还知道我的博客是关于技术的😎，看来这次的训练样本中包含我的信息啊……所以我对这次的更新也挺有好感，毕竟我的知识如果能成为AI的一部分，也算是一种永恒吧。&lt;/p&gt;

&lt;h1 id=&quot;在8gib内存的macbook运行的新模型&quot;&gt;在8GiB内存的MacBook运行的新模型&lt;/h1&gt;
&lt;p&gt;在3年前，我在&lt;a href=&quot;/2023/04/05/ai.html&quot;&gt;探索AI&lt;/a&gt;时，在我只有8GiB内存的&lt;a href=&quot;/2023/02/03/mbp.html&quot;&gt;MacBook Pro&lt;/a&gt;上运行了非常早期的LLM——Alpaca-7B，那时候7B的LLM虽然能回答一些问题，但答非所问的情况也非常多。不过最近我发现了一个有意思的LLM，叫做&lt;a href=&quot;https://huggingface.co/LiquidAI/LFM2.5-1.2B-Thinking&quot;&gt;LFM2.5-1.2B-Thinking&lt;/a&gt;，它只用了12亿的参数就有思维链，而且水平据说还挺强。这么长时间过去之后我倒也想看看我的MacBook能运行多聪明的模型，所以就试着跑了一下它。 &lt;br /&gt;
  运行它也很容易，一般用Ollama就可以，但是Ollama只有TUI，不能渲染Markdown，我也不太想在我的Mac上整WebUI之类的东西……那有什么好的选择吗？我去制作这个模型的公司官网看了一下，他们制作这个模型本就是为了在端侧运行，所以也专门制作了一个软件运行他们的模型，叫做&lt;a href=&quot;https://www.liquid.ai/apollo&quot;&gt;Apollo&lt;/a&gt;，在手机和Mac上都可以用。我在我的Mac上安装试了一下，效果很好，首先速度非常快，8bit量化正常情况下可以达到60多Tps，即使是省电模式，也能达到20多Tps。另外加上思维链它的思考能力也还不错，虽然一些脑筋急转弯的题不算擅长，但是正常对话，回答问题之类的表现都很不错，相比于之前7B的模型表现好太多了。当然考虑到都已经过去3年了，能有这样的进步也很正常，不过12亿参数就能有这样的智能还是相当可以啊。 &lt;br /&gt;
  这个模型之所以有这样的能力似乎是因为他们并不完全是Transformer架构，而是使用的一种叫做LFM2的混合架构，按照大家对他们公司（Liquid AI）以及这个架构名字的理解，可能会觉得这个模型基于液态神经网络，不过我让AI看了一下他们的代码似乎并不是，他们用的是一种类似于Mamba的架构，这种架构似乎就很擅长在小参数的模型下比Transformer模型表现的更好，所以说这种变化也是算法进步带来的。 &lt;br /&gt;
  顺便一说这个Apollo除了运行他们自己的模型之外也能连接其他兼容OpenAI接口的模型，正好可以用来连接我的GPT-OSS，这样我就可以不需要下载一些浏览器套壳的重型应用来用我的模型了😝。&lt;/p&gt;

&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;自从ChatGPT之后，AI的发展真是越来越强了，而且能看出来目前甚至并不需要多新多好的硬件就能让一般人获得还不错的智能（当然训练也许还是要大量的硬件），这么看来AI软件的发展还是相当有潜力。目前来看既然优化软件就能做得越来越好，那也许在有限的硬件环境下可以期待无限的智能吧。&lt;/p&gt;</content><author><name>mayx</name></author><category term="AI" /><category term="LLM" /><category term="模型部署" /><category term="使用体验" /><summary type="html">用更多的方式探索AI！</summary></entry><entry><title type="html">在Google杀死XSLT之后的XML美化方案</title><link href="/2026/02/08/xslt.html" rel="alternate" type="text/html" title="在Google杀死XSLT之后的XML美化方案" /><published>2026-02-08T00:00:00+08:00</published><updated>2026-02-08T00:00:00+08:00</updated><id>/2026/02/08/xslt</id><content type="html" xml:base="/2026/02/08/xslt.html">&lt;p&gt;即使没有了XSLT，也不能让读者看到光秃秃的XML！&lt;!--more--&gt;&lt;/p&gt;

&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在半年前，我写了一篇&lt;a href=&quot;/2025/07/01/xslt.html&quot;&gt;用XSLT美化博客XML文件&lt;/a&gt;的文章，自从那以后，每次我在浏览其他人博客的时候，都会看一眼对方博客有没有给自己的订阅文件做美化。不过就在前段时间，我在浏览某个博客的时候，发现他博客的订阅文件，甚至连最基本的XML文档树都没有显示出来。这时候我打开开发者工具看了一眼源代码，发现他也并没有使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xml-stylesheet&lt;/code&gt;之类的指令……而且控制台貌似报了些错，好像是出现了什么CSP错误……于是我就想，浏览器显示XML文档树的本质，会不会其实也是一种XSLT？之所以报错也有可能是浏览器在自动引用内置的XSLT时违反了CSP。所以我就问了问谷歌AI，结果似乎真的是这样，比如火狐浏览器就内置了一份&lt;a href=&quot;https://github.com/mozilla-firefox/firefox/blob/main/dom/xml/resources/XMLPrettyPrint.xsl&quot;&gt;XSLT文件&lt;/a&gt;，IE浏览器也有。正当我为XSLT的功能感到强大时，谷歌AI随后提到，&lt;a href=&quot;https://developer.chrome.com/docs/web-platform/deprecating-xslt&quot;&gt;Chrome浏览器决定弃用XSLT&lt;/a&gt;，所以以后不要再用XSLT了😰…… &lt;br /&gt;
  我给我的订阅文件加美化功能才半年，怎么就要不能用了？XSLT出现这么多年都还能用，结果等我加上就要废弃了？当时为了增加这个功能，还是费了不少劲的，怎么能让谷歌说没就没？于是我就开始对这件事进行了调查。&lt;/p&gt;

&lt;h1 id=&quot;google杀死了xslt&quot;&gt;Google杀死了XSLT&lt;/h1&gt;
&lt;p&gt;从上面Chrome的弃用XSLT文档中，可以发现，这件事的始作俑者是&lt;a href=&quot;https://github.com/mfreed7&quot;&gt;Mason Freed&lt;/a&gt;，他在WHATWG中发起了一个&lt;a href=&quot;https://github.com/whatwg/html/issues/11523&quot;&gt;Issue&lt;/a&gt;，因为XSLT用的人很少，以及实现XSLT的库很老而且容易出漏洞，所以建议把XSLT从Web标准中删除。在这个Issue中可以发现，有很多人表示不满，毕竟这个功能对想要给自己订阅做美化的博主来说还是很有用的。为了对抗谷歌，还有人做了个网站： &lt;a href=&quot;https://xslt.rip&quot;&gt;https://xslt.rip&lt;/a&gt; 。 &lt;br /&gt;
  而且XSLT虽然用的人占比也许不高，但从总量上应该还是挺多的，除了用XSLT美化博客订阅的，甚至还有用&lt;a href=&quot;https://github.com/vgr-land/vgr-xslt-blog-framework&quot;&gt;XSLT作为博客框架的&lt;/a&gt;，另外还有一些人提出&lt;a href=&quot;https://github.com/whatwg/html/issues/11582&quot;&gt;一部分政府网站也有使用XSLT&lt;/a&gt;。 &lt;br /&gt;
  不过Freed看起来对这件事早有准备，他做了一个&lt;a href=&quot;https://github.com/mfreed7/xslt_polyfill&quot;&gt;Polyfill库&lt;/a&gt;，通过WASM的方式让XSLT可以正常工作，为了方便大家使用这个库，我顺手给CDNJS发了个&lt;a href=&quot;https://github.com/cdnjs/packages/pull/2118&quot;&gt;PR&lt;/a&gt;，以后可以用CDN引用它了。不过使用这个库的前提是需要在订阅中加一段引用JS的代码，像我博客中的Atom订阅，用的是&lt;a href=&quot;https://github.com/jekyll/jekyll-feed&quot;&gt;jekyll-feed&lt;/a&gt;插件，里面的格式都是写死的，就用不了了…… &lt;br /&gt;
  只不过现在已经没办法阻止谷歌了……而且其他浏览器也表示会跟进，看来我们唯一能做的就是去适应了。&lt;/p&gt;

&lt;h1 id=&quot;没有xslt之后的美化方案&quot;&gt;没有XSLT之后的美化方案&lt;/h1&gt;
&lt;h2 id=&quot;纯css&quot;&gt;纯CSS&lt;/h2&gt;
&lt;p&gt;虽然XSLT不能用，但不代表&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xml-stylesheet&lt;/code&gt;指令就不能用了，除了XSLT之外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xml-stylesheet&lt;/code&gt;同样可以引用CSS。只是似乎完全没见过用CSS美化订阅源的，也许是因为光用CSS能做到的事比较少吧，想用CSS给XML文档加链接之类的估计就做不到了。 &lt;br /&gt;
  但目前能选择的也不多了，既然大家都没写过用CSS美化订阅源，那就让我来写一个吧！然而我并不会写😅……那就只好让AI来写了，我把需求说清楚之后，AI就写出来了：&lt;a href=&quot;/assets/css/feed.css&quot;&gt;feed.css&lt;/a&gt;。试了一下效果还挺不错的，我让AI写的这个版本无论是RSS还是Atom都可以使用，如果有人感兴趣可以拿去用。可惜我的Atom订阅因为用的是插件的原因用不了😭，只能加到用纯Liquid实现的RSS订阅上了。 &lt;br /&gt;
  但用纯CSS的缺点也很明显，没办法操作文档的内容，像修改日期格式的就做不了了，而且也不能添加超链接……XML的标签本身对浏览器来说并没有内建的语义，正常情况下也没法让浏览器把某个标签当作超链接。那难道就没办法了吗？&lt;/p&gt;
&lt;h2 id=&quot;混合xhtml&quot;&gt;混合XHTML&lt;/h2&gt;
&lt;p&gt;如果完全不能修改XML内容，那确实就没有办法了，但如果能修改XML的内容那还是有办法的，简单来说就是混入XHTML，事实上Freed编写的Polyfill库原理上也是利用了XHTML，只要在能作为XHTML的标签中添加XHTML的命名空间，那么浏览器就可以理解它的语义并渲染，像刚刚用纯CSS美化的订阅没有链接，那就可以在根元素中添加命名空间：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xmlns:xhtml=&quot;http://www.w3.org/1999/xhtml&quot;&lt;/code&gt;，然后在合适的位置写：&lt;/p&gt;
&lt;div class=&quot;language-xml 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;nt&quot;&gt;&amp;lt;xhtml:a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Read more -&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/xhtml:a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;就可以了。只是这样有个缺点，这样写的订阅文件不够“纯粹”，用验证器验证会显示“&lt;a href=&quot;https://validator.w3.org/feed/docs/warning/MisplacedXHTMLContent.html&quot;&gt;Misplaced XHTML content&lt;/a&gt;”警告。对有洁癖的人来说可能会有点难受😆。 &lt;br /&gt;
  不过如果能接受这种“不纯粹”，那么其实&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xml-stylesheet&lt;/code&gt;指令也没必要了，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link&lt;/code&gt;标签一样可以用，包括&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;script&lt;/code&gt;也是，所以有人写了一个&lt;a href=&quot;https://github.com/dfabulich/style-xml-feeds-without-xslt&quot;&gt;不使用XSLT美化XML&lt;/a&gt;的库。 &lt;br /&gt;
  只不过这种方法和XSLT相比还是有一些缺陷，要知道XSLT的本质是转换，是把XML转换为HTML，也就是说转出来的文档本质是HTML，所有的DOM操作都和操作HTML是完全相同的，但是在XML里混入XHTML标签就不一样了，它的本质依然是XML文档，只是嵌入了XHTML命名空间下的元素，所以相应的DOM操作会有一些不同。如果是自己写的纯JS可能还好，如果是用了jQuery之类假定DOM为HTML的库就会出现问题了，因此这也就是那个Polyfill库的局限性，用正常的XSLT执行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.constructor&lt;/code&gt;会显示&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTMLDocument&lt;/code&gt;，而用这个Polyfill库执行完则是显示&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XMLDocument&lt;/code&gt;。因此，直接套用为浏览器原生XSLT编写的旧样式文件，就有可能会出问题，但如果要考虑改XSLT的话那还不如重新写JS，然后用XHTML引入呢。&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="XML" /><category term="Feed" /><category term="XSLT" /><category term="美化" /><summary type="html">即使没有了XSLT，也不能让读者看到光秃秃的XML！</summary></entry><entry><title type="html">年终总结</title><link href="/2026/01/01/summary.html" rel="alternate" type="text/html" title="年终总结" /><published>2026-01-01T00:00:00+08:00</published><updated>2026-01-01T00:00:00+08:00</updated><id>/2026/01/01/summary</id><content type="html" xml:base="/2026/01/01/summary.html">&lt;p&gt;0 error(s), ∞ warning(s)&lt;!--more--&gt;&lt;/p&gt;

&lt;h1 id=&quot;2025年的状态&quot;&gt;2025年的状态&lt;/h1&gt;
&lt;p&gt;在2025年，感觉状态不如去年……由于没能做出正确的选择，还是有点糟糕。不过总的来说还没有引发关键性的错误，至少还能继续坚持下去。 &lt;br /&gt;
  在这一年中，感觉记忆和思考能力都有所下滑，看来是没把自己照顾好😂，不过看看这一年写的文章，看起来似乎比以前更流畅了，这也许是因为和AI聊得多了，以至于思维有点偏向AI了吧。 &lt;br /&gt;
  总的来说感觉自己的稳定性还是有点低了，但这可能不是我能独自解决的，也不知会有什么转机……&lt;/p&gt;

&lt;h1 id=&quot;2025年发生的事情&quot;&gt;2025年发生的事情&lt;/h1&gt;
&lt;p&gt;回顾了一下&lt;a href=&quot;/2025/01/01/summary.html&quot;&gt;去年的年终总结&lt;/a&gt;，发现自己还是没能做到知行合一，在这一年里全球各类资产突然开始大幅升值，也就是说钱真的开始不值钱了……那时候想着买黄金，这一年下来却没能下定决心，最终错过了资产保值的机会。至于现在，似乎什么也做不了了……当然这对我的生活并没有造成什么严重的打击，只是感受到环境对自己的影响罢了。 &lt;br /&gt;
  至于AI……依然是一天比一天强，而各个公司对AI的投入相比去年也是极大的提升，当然出来的效果也是非常强，那时候的AI还是挺容易出错，但是现在AI解决问题的能力已经可以替代很多人了，不只是文本生成模型，今年的图像与视频生成模型也真的是发展到了以往完全不能想象的地步，真的可以做到一句话想要什么就有什么了。 &lt;br /&gt;
  另外，今年写的博客内容过于围绕博客本身了，以至于似乎不太跟得上时代，虽然我的博客也确实有点老旧了😆。只是看看以前的文章，都还有一些面向未来的趋势，而今年就有点“考古”了。相比于考古，去展望未来显然是更有意义的事情，只不过……真的感觉脑子不太好使，未来会发生什么，已经完全无法预测了。&lt;/p&gt;

&lt;h1 id=&quot;展望2026年&quot;&gt;展望2026年&lt;/h1&gt;
&lt;p&gt;虽然不知道未来会发生什么，但毕竟还没有造成关键性的错误，还有修正的余地，只能希望未来能够做出正确的选择，不要让自己陷入危险的境地吧。&lt;/p&gt;</content><author><name>mayx</name></author><category term="总结" /><summary type="html">0 error(s), ∞ warning(s)</summary></entry><entry><title type="html">在浏览器中运行Linux的各种方法</title><link href="/2025/12/01/linux.html" rel="alternate" type="text/html" title="在浏览器中运行Linux的各种方法" /><published>2025-12-01T00:00:00+08:00</published><updated>2025-12-01T00:00:00+08:00</updated><id>/2025/12/01/linux</id><content type="html" xml:base="/2025/12/01/linux.html">&lt;p&gt;浏览器已经无所不能了！&lt;!--more--&gt;&lt;/p&gt;

&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;前段时间跟网友交流时，有人展示了他博客里的一个Linux终端模拟项目：&lt;a href=&quot;https://github.com/Erzbir/jsnix&quot;&gt;jsnix&lt;/a&gt;，看起来挺有意思的，里面甚至还藏了一个CTF。不过我感觉他这个终端和博客本身并没有真正联动起来，本质上只是一个模拟了Linux Shell行为的交互界面。除此之外我还发现了另一个风格类似的&lt;a href=&quot;https://github.com/Luyoung0001/myWebsite&quot;&gt;个人主页&lt;/a&gt;，它虽然也走了终端风格，但功能更简单，还原度也不算高。不过它至少和博客内容做了一些基础联动——尽管目前也只是做到列出文章这种程度😂，当然有这类功能的博客应该也不少，只是我发现的不太多……于是我就想，不如我也给自己的博客加一个类似的“命令行访问”功能，应该会很有趣。当然如果真要做的话，我肯定不会满足于只实现几个模拟指令——既然要做，就要追求真实感，至少得在浏览器上运行真实的Linux终端，才不会让人觉得出戏吧😋。&lt;/p&gt;

&lt;h1 id=&quot;在浏览器中运行linux&quot;&gt;在浏览器中运行Linux&lt;/h1&gt;
&lt;h2 id=&quot;虚拟机方案&quot;&gt;虚拟机方案&lt;/h2&gt;
&lt;h3 id=&quot;纯js虚拟机&quot;&gt;纯JS虚拟机&lt;/h3&gt;
&lt;p&gt;要说到在浏览器上运行Linux，最先想到的应该就是&lt;a href=&quot;https://bellard.org&quot;&gt;Fabrice Bellard&lt;/a&gt;大神写的&lt;a href=&quot;https://bellard.org/jslinux/&quot;&gt;JSLinux&lt;/a&gt;吧，这可能是第一个在浏览器中实现的虚拟机（毕竟是最强虚拟机QEMU的作者编写的）。现在他的个人主页中展示的这个版本是WASM版本，而他最早写的是纯JS实现的。那个JS实现的版本现在在GitHub上有一个&lt;a href=&quot;https://github.com/levskaya/jslinux-deobfuscated&quot;&gt;去混淆的版本&lt;/a&gt;可以用作学习和研究，于是我顺手Fork了一份在GitHub Pages上部署作为&lt;a href=&quot;http://mabbs.github.io/jslinux/&quot;&gt;演示&lt;/a&gt;。 &lt;br /&gt;
  作为纯JS实现的x86虚拟机，性能估计是最差的，但相应的兼容性也最好，在Bellard当年写JSLinux的时候，还没有WASM这种东西呢，所以即使是在不支持WASM的IE11中，也可以正常运行。假如我想把它作为终端用在我的博客上，似乎也是个不错的选择，即使我完全看不懂代码，不知道如何实现JS和虚拟机的通信，它也预留了一个剪贴板设备，可以让我轻松地做到类似的事情，比如我在里面写个Bash脚本，通过它和外面的JS脚本联动来读取我的文章列表和内容，那也挺不错。 &lt;br /&gt;
  当然Bellard用纯JS编写虚拟机也不是独一份，他实现了x86的虚拟机，相应的也有人用纯JS实现了RISC-V的虚拟机，比如&lt;a href=&quot;https://github.com/riscv-software-src/riscv-angel&quot;&gt;ANGEL&lt;/a&gt;，看起来挺不错，所以同样也顺手&lt;a href=&quot;https://mabbs.github.io/riscv-angel/&quot;&gt;搭了一份&lt;/a&gt;。只不过它似乎用了一些更先进的语法，至少IE11上不能运行。 &lt;br /&gt;
  另外还有一个比较知名的项目，叫做&lt;a href=&quot;https://github.com/s-macke/jor1k&quot;&gt;jor1k&lt;/a&gt;，它模拟的是OpenRISC架构。只是这个架构目前已经过时，基本上没什么人用了，不过这里面还内置了几个演示的小游戏，看起来还挺有意思。 &lt;br /&gt;
  除了这些之外，其实能在浏览器上运行的Linux也不一定是个网页，有一个叫做&lt;a href=&quot;https://github.com/ading2210/linuxpdf&quot;&gt;LinuxPDF&lt;/a&gt;的项目可以让Linux运行在PDF中，它的原理和JSLinux差不多，所以需要PDF阅读器支持JS，看它的介绍貌似只能在基于Chromium内核的浏览器中运行，而且因为安全问题在PDF中有很多功能不能用，所以它的速度甚至比JSLinux还要慢，功能还很少，因此它基本上只是个PoC，没什么太大的意义。&lt;/p&gt;
&lt;h3 id=&quot;wasm虚拟机&quot;&gt;WASM虚拟机&lt;/h3&gt;
&lt;p&gt;那还有别的方案吗？既然Bellard都选择放弃纯JS的JSLinux而选择了WASM，显然还有其他类似的项目，比如&lt;a href=&quot;https://github.com/copy/v86&quot;&gt;v86&lt;/a&gt;，这也是一个能在浏览器中运行的x86虚拟机，不过因为使用了WASM和JIT技术，所以效率要比纯JS的JSLinux高得多。另外作为虚拟机，自然是不止能运行Linux，其他的系统也能运行，在示例中除了Linux之外还有DOS和Windows之类的系统，功能还挺强大，如果能自己做个系统镜像在博客里运行，似乎也是不错的选择。 &lt;br /&gt;
  另外还有一个相对比较知名的叫&lt;a href=&quot;https://github.com/leaningtech/webvm&quot;&gt;WebVM&lt;/a&gt;，从效果上来说和v86几乎没有区别，同样使用了WASM和JIT技术，也都只支持32位x86，然而它的虚拟化引擎CheerpX是闭源产品，既然和v86都拉不开差距，不知道是谁给他们的信心把它作为闭源产品😅。不过看它的说明文档，其相比于v86的主要区别是实现了Linux系统调用，考虑到它不能运行其他操作系统，而且Linux内核也不能更换，那我想它可能是类似于WSL1的那种实现方案，也许性能上会比v86好一些吧……只不过毕竟是闭源产品，不太清楚具体实现了。 &lt;br /&gt;
  既然纯JS有RISC-V的虚拟机，WASM当然也有，比如&lt;a href=&quot;https://github.com/edubart/webcm&quot;&gt;WebCM&lt;/a&gt;。这个项目相比于其他的项目有个不太一样的地方，它把虚拟机、内核以及镜像打包成了一个单独的WASM文件……只是这样感觉并没有什么好处吧，改起来更加复杂了。 &lt;br /&gt;
  以上这些虚拟机方案各有不同，但是想做一个自己的镜像相对来说还是有点困难，于是我又发现了另一个项目：&lt;a href=&quot;https://github.com/container2wasm/container2wasm&quot;&gt;container2wasm&lt;/a&gt;，它可以让一个Docker镜像在浏览器中运行，当然实际实现其实和Docker并没有什么关系，本质还是虚拟机，只是制作镜像的时候可以直接用Docker镜像，方便了不少，但Docker镜像一般也都很大，所以第一次加载可能要下载很长时间。另外它还有一个优势，可以使用&lt;a href=&quot;https://bochs.sourceforge.io/&quot;&gt;Bochs&lt;/a&gt;运行x86_64的镜像，不像v86和WebVM只能模拟32位的x86（虽然Bochs的运行效率可能会差一些），而且可以使用WASI直接访问网络，不像以上几个项目如果需要访问网络需要用到中继服务。当然访问网络这个还是要受浏览器本身的跨域策略限制。总之从项目本身来说感觉也算是相当成熟了，尤其能用Docker镜像的话……我甚至可以考虑直接用&lt;a href=&quot;https://hub.docker.com/r/unmayx/mabbs&quot;&gt;镜像&lt;/a&gt;在线演示我曾经的&lt;a href=&quot;https://github.com/Mabbs/Mabbs.Project&quot;&gt;Mabbs&lt;/a&gt;项目😋。&lt;/p&gt;
&lt;h2 id=&quot;纯wasm方案&quot;&gt;纯WASM方案&lt;/h2&gt;
&lt;p&gt;其实想要在浏览器中运行Linux也不一定非得要用虚拟机，用虚拟机相当于是把其他指令集的机器码翻译为WASM，然后浏览器还得再翻译成宿主机CPU支持的指令集，然而WASM本身其实也算是一种指令集，各种编译型语言编写的程序也能编译出WASM的产物，比如&lt;a href=&quot;https://github.com/ffmpegwasm/ffmpeg.wasm&quot;&gt;FFmpeg&lt;/a&gt;。所以Linux内核也完全可以被编译成WASM，正好前段时间我看新闻说&lt;a href=&quot;https://github.com/joelseverin&quot;&gt;Joel Severin&lt;/a&gt;做了这么一个&lt;a href=&quot;https://github.com/joelseverin/linux-wasm&quot;&gt;项目&lt;/a&gt;，对Linux内核做了一些修改使其可以被编译为WASM程序，我试了一下，貌似在Safari浏览器中不能正常工作……Chrome浏览器倒是没问题，不过即使这样用起来BUG也很多，随便执行几条命令就会冻结，体验不是很好。 &lt;br /&gt;
  沿着这个项目，我又找到一个由&lt;a href=&quot;https://github.com/tombl&quot;&gt;Thomas Stokes&lt;/a&gt;制作的&lt;a href=&quot;https://github.com/tombl/linux&quot;&gt;项目&lt;/a&gt;，和Joel的项目差不多，但我测了一下可以在Safari上运行，感觉这个项目更完善，不过之前那个项目上了新闻，所以⭐️数比这个更高😂。 &lt;br /&gt;
  于是我把它复制了一份，在我的GitHub Pages上&lt;a href=&quot;https://mabbs.github.io/linux/&quot;&gt;部署&lt;/a&gt;了，但直接用仓库中的源代码会显示“Error: not cross origin isolated”，然而在Thomas自己部署的网站中可以正常打开，我看了一眼貌似是因为在GitHub Pages中没有&lt;a href=&quot;https://web.dev/articles/coop-coep&quot;&gt;COOP和COEP响应头&lt;/a&gt;导致的。Linux作为多任务操作系统来说，当然要运行多个进程，而Linux要管理它们就需要跨线程（Web Worker）读取内存的能力，所以用到了SharedArrayBuffer对象。不过由于CPU曾经出过“幽灵”漏洞，导致现代浏览器默认禁止使用SharedArrayBuffer对象，除非在服务器中配置COOP和COEP响应头才可以用，但是Joel的项目也是在GitHub Pages上运行的啊，为什么可以正常运行？看了源代码后才发现原来可以&lt;a href=&quot;/2025/08/01/sw-proxy.html&quot;&gt;用Service Worker作为反向代理&lt;/a&gt;来给请求的资源加上响应头，他使用的是&lt;a href=&quot;https://github.com/gzuidhof/coi-serviceworker&quot;&gt;coi-serviceworker&lt;/a&gt;这个项目，所以我也给我部署的代码中加上了这个脚本，总算是解决了这个问题。 &lt;br /&gt;
  部署好这个项目之后我试用了几下，虽然有些操作仍然会导致系统冻结，但相比Joel的版本来说已经好多了。很遗憾的是目前这个WASM Linux还不能和外界通信，所以作用不是很大，另外如果想在里面运行其他二进制程序还是相当困难，首先在WASM中不存在内存管理单元（MMU），不能实现隔离和分页的功能，另外以WASM作为指令集的环境下编译的产物也得是WASM，所以目前来说想用它做点什么还是不太合适。 &lt;br /&gt;
  以上的这两个将Linux内核编译为WASM的方案其实相当于给内核打补丁，然后把浏览器看作是虚拟机来运行，有点像Xen，不过还有一种让Linux原生运行在WASM的&lt;a href=&quot;https://github.com/okuoku/wasmlinux-project&quot;&gt;项目&lt;/a&gt;，它将&lt;a href=&quot;https://github.com/lkl/linux&quot;&gt;Linux kernel library&lt;/a&gt;编译为了WASM。那么什么是LKL？简单来说它有点像Wine，就和我之前所说的&lt;a href=&quot;/2024/12/08/simulator.html&quot;&gt;OS模拟器&lt;/a&gt;差不多，可以提供一个环境，让程序以为自己在Linux下运行，所以说它和之前的实现有一些不一样，它不存在内核模式，更像是一个普通的程序，而不是系统了。 &lt;br /&gt;
  不过这个项目的体验也比较一般，它无论做什么都得按两次回车，看说明的意思貌似是因为没有实现异步信号传递，所以要手动打断&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read&lt;/code&gt;函数，而且也经常莫名其妙卡住，总体体验不如Thomas的项目。&lt;/p&gt;
&lt;h2 id=&quot;模仿的linux&quot;&gt;模仿的Linux&lt;/h2&gt;
&lt;p&gt;其实如果只是想做到和Linux类似的功能，也有这样的项目，比如&lt;a href=&quot;https://github.com/stackblitz/webcontainer-core&quot;&gt;WebContainers&lt;/a&gt;，它没有运行Linux系统，但是模拟了一个环境，可以在浏览器中运行Node.js以及Python之类的脚本，而且让脚本以为自己在Linux中运行，除此之外它还能用Service Worker把环境中运行的端口映射给浏览器，可以算是真的把服务端跑在浏览器上了。这个技术还挺高级，不过想想也挺合理，毕竟有WASI，直接编译为WASM的程序也不需要操作系统就能运行，所以用WASM去运行Linux本来就有点多此一举了😂。不过很遗憾的是WebContainers也不是开源软件，要使用它只能引入StackBlitz的资源，而且全网完全没有开源的替代品……也许在浏览器上进行开发本来就是个伪需求，所以没什么人实现吧。 &lt;br /&gt;
  当然如果只是实现和WebContainers类似的功能，&lt;a href=&quot;https://github.com/jupyterlite/jupyterlite&quot;&gt;JupyterLite&lt;/a&gt;也可以实现，它可以在浏览器中像使用本地JupyterLab那样运行JS和Python，还能用Matplotlib、Numpy、Pandas进行数据处理，功能可以说非常强大，而且还是开源软件。只不过它没有模拟操作系统的环境，所以不能运行Node.js项目，也不能提供终端，所以不太符合我想要的效果……&lt;/p&gt;

&lt;h1 id=&quot;总结&quot;&gt;总结&lt;/h1&gt;
&lt;p&gt;总的来说，如果想要在博客上搞Linux终端，目前来看似乎虚拟机方案会更靠谱一些，虽然相对来说效率可能比较低，但毕竟目前WASM方案的可靠性还是不够，而且考虑到还需要配置额外的响应头，感觉有点麻烦，当然我觉得WASM还是算未来可期的，如果成熟的话肯定还是比虚拟机要更好一些，毕竟没有转译性能肯定要好不少。至于WebContainers这种方案……等什么时候有开源替代再考虑吧，需要依赖其他服务感觉不够可靠。只是也许我的想法只需要模拟一个合适的文件系统，然后给WASM版的Busybox加个终端就够了？不过这样感觉Bug会更多😂。 &lt;br /&gt;
  至于打算什么时候给博客加上这个功能？应该也是未来可期吧😝，目前还没什么好的思路，仅仅是分享一下在浏览器中运行Linux的各种方法。&lt;/p&gt;</content><author><name>mayx</name></author><category term="浏览器" /><category term="Linux" /><category term="虚拟机" /><category term="WASM" /><summary type="html">浏览器已经无所不能了！</summary></entry><entry><title type="html">让博客永恒的探索</title><link href="/2025/11/01/mirrors.html" rel="alternate" type="text/html" title="让博客永恒的探索" /><published>2025-11-01T00:00:00+08:00</published><updated>2025-11-01T00:00:00+08:00</updated><id>/2025/11/01/mirrors</id><content type="html" xml:base="/2025/11/01/mirrors.html">&lt;p&gt;Mayx Forever Project – Phase II&lt;!--more--&gt;&lt;/p&gt;

&lt;h1 id=&quot;起因&quot;&gt;起因&lt;/h1&gt;
&lt;p&gt;在前段时间，我通过&lt;a href=&quot;https://github.com/ecosyste-ms/repos&quot;&gt;Ecosyste.ms: Repos&lt;/a&gt;找到了不少Git平台的实例，也在探索的过程中发现和了解了&lt;a href=&quot;/2025/08/10/tilde.html&quot;&gt;Tilde社区&lt;/a&gt;。当然仅仅是这样显然还不够，里面的实例太多了，显然还有一些其他值得探索的东西。 &lt;br /&gt;
  在我查看这里面的某些Gitea实例时，发现了一些奇怪的事情，有些实例的仓库数和用户数多得离谱，正常来说除了几个大的平台，绝大多数应该只有几十到几百个仓库，这就让我有点好奇了。于是当我点进去之后发现，里面有一大堆仓库都是空的，而且用户名和仓库名都非常有规律，看起来都是一组单词加4位数字命名的，显然这不是正常现象，应该是一种有组织的行为。&lt;/p&gt;

&lt;h1 id=&quot;被spam滥用的git实例&quot;&gt;被SPAM滥用的Git实例&lt;/h1&gt;
&lt;p&gt;于是我就简单看了一下这些异常的仓库和用户的规律，可以发现每个用户都填了个人主页地址，然后个人简介里大都是一段广告词。另外这些个人主页的地址看起来很多都是利用公开可注册的服务，比如开源的有各种Git平台、Wiki，以及论坛，还有一些允许用户写个人主页的新闻网站。在这其中，Git平台大多都没有广告文章，基本上都是通过个人主页地址链接到网站，而Wiki之类的就会写一些篇幅比较长的广告文章。 &lt;br /&gt;
  另外这些平台但凡还在开放注册，就会被以大约每分钟一次的速度自动注册新账号……所以这种事情到底是谁在干呢？我翻了几个仓库，里面的广告多种多样，有些看起来还算正常，还有一些看起来有些黑产。其中我发现有一家叫做“悠闲羊驼SEO”的网站，看介绍主要是给加密货币、对冲基金和博彩网站提供SEO优化的，再加上这些被滥用的平台里也有不少类似的广告，所以我怀疑这些滥用的行为就是这家SEO公司做的（虽然没有证据😂）。&lt;/p&gt;

&lt;h1 id=&quot;永恒的探索&quot;&gt;永恒的探索&lt;/h1&gt;
&lt;p&gt;看到这么多Git平台被滥用，我就有个想法，之前为了保证可靠性给博客加了不少&lt;a href=&quot;/proxylist.html&quot;&gt;镜像&lt;/a&gt;，除此之外也在互联网档案馆、&lt;a href=&quot;https://archive.softwareheritage.org/&quot;&gt;Software Heritage&lt;/a&gt;、Git Protect等存档服务中上传了备份，而且也在IPFS和Arweave等Web3平台上有相应的副本，但是我觉得还不够，再大的平台也有可能会倒闭，IPFS不Pin还会被GC，至于Arweave前段时间看了一眼整个网络才几百个节点，感觉一点也不靠谱……所以我应该好好利用这些平台提高我博客的可靠性。 &lt;br /&gt;
  既然那些Spammer只是为了SEO去滥用这些平台，不如让我利用这些平台给我的博客进行镜像吧！至于使用哪个平台……显然用Git平台方便一些，所以接下来就该考虑一下怎么样分发了。&lt;/p&gt;

&lt;h1 id=&quot;镜像的分发&quot;&gt;镜像的分发&lt;/h1&gt;
&lt;p&gt;在Git平台中也有很多选择，最知名的是GitLab，不过GitLab有点复杂，接口不太好用……而且很多实例没有开镜像仓库的功能，毕竟如果我每次更新都给一堆仓库推送太费时间了，我打算让各个平台主动从GitHub上拉取我的最新代码。正好Gogs系列的平台基本上都默认支持镜像仓库，不过在我实际使用的时候发现Gogs默认情况下注册要验证码……写识别验证码感觉又挺麻烦，而Gogs的两个分支——Gitea和Forgejo反倒没有……还挺奇怪，所以接下来我的目标主要就是Gitea和Forgejo的实例了。 &lt;br /&gt;
  既然决定好目标，我就得先发现它们了，那些Spammer在注册的时候会在个人主页里写不同的网站，其中也有一些类Gogs平台，那么我可以先找一个Gitea平台，用接口读取这些网站，然后再调类Gogs专属的接口来检测这些网站哪个是类Gogs平台，于是我就写了个&lt;a href=&quot;https://github.com/Mabbs/spam_gogs-like_scanner/blob/main/main.py&quot;&gt;脚本&lt;/a&gt;来找到它们。 &lt;br /&gt;
  找到这些平台之后就该注册了，还好Gitea和Forgejo默认没有验证码，注册起来也很简单，随便写了个函数实现了一下：&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;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;register_account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&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;n&quot;&gt;resp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user/sign_up&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;soup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BeautifulSoup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;html.parser&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;csrf_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;soup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&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;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;_csrf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&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;s&quot;&gt;&quot;_csrf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;csrf_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;retype&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&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;n&quot;&gt;headers&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;s&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;application/x-www-form-urlencoded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;resp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/user/sign_up&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&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;s&quot;&gt;&quot;flash-success&quot;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&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;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Successfully registered at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; with username: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;, email: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;, password: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;save_to_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;instances_userinfo.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&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;bp&quot;&gt;True&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;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Failed to register at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.&quot;&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;bp&quot;&gt;False&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&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;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Error registering at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&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;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;注册完之后就该导入仓库了，只是通过模拟前端发包的方式在Gitea和Forgejo中不同版本的表现可能不太一样，所以我想用API实现，但是API又得有API Key，生成API Key还得模拟前端发包😥……所以怎么都绕不过。 &lt;br /&gt;
  不过这个生成API Key还挺麻烦，有些版本不需要配权限范围，有些配权限的参数还不一样……不过我就是随便一写，凑合用吧，像那些专业的Spammer应该是有更强大的脚本判断各种情况。 &lt;br /&gt;
  最后我还是选择用API导入，又写了个函数：&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;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;import_repos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&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;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/api/v1/repos/migrate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;headers&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;s&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;token &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&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;n&quot;&gt;json&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;s&quot;&gt;&quot;repo_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;blog&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;mirror_interval&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1h&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;mirror&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Mayx's Home Page&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;clone_addr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/Mabbs/mabbs.github.io&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;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;201&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;s&quot;&gt;&quot;Repository import initiated successfully.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;save_to_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;repo_list.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/mayx/blog&quot;&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;bp&quot;&gt;True&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;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Failed to initiate repository import. Status code: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&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;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Response: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&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;bp&quot;&gt;False&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&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;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Error updating website: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&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;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;脚本写好之后我就只需要重复扫描、注册、导入的步骤就行了，这样我的镜像就会越来越多，而且用类Gogs的实例还有一个好处就是不需要我手动推送，它会自动定时拉取我的仓库保持最新，这样也许只要人类文明存在我的博客就会在某处存在吧🤣。 &lt;br /&gt;
  最后我创建的Git镜像可以在&lt;a href=&quot;/other_repo_list.html&quot;&gt;这里&lt;/a&gt;看到，看起来还是挺壮观啊😋。只不过像这种会被Spammer随便注册的Git平台实例很难说它能活多久，如果没人管而且是云服务器也许到期就没了，有人管的话应该不会允许这么多Spam行为吧……&lt;/p&gt;

&lt;h1 id=&quot;感想&quot;&gt;感想&lt;/h1&gt;
&lt;p&gt;不知道用“量”来确保博客的永恒更可靠……还是用“质”的方式更好呢？其实我觉得还得是活动的更好，就像我以前所说的，如果有&lt;a href=&quot;/2024/11/02/trojan.html#%E6%84%9F%E6%83%B3&quot;&gt;僵尸网络&lt;/a&gt;，自动帮我执行发现并推送的操作，也许比等着这些实例逐渐消失更好吧……只不过那样可能就不太友好了😂。&lt;/p&gt;</content><author><name>mayx</name></author><category term="Git" /><category term="Gitea" /><category term="镜像" /><category term="Forever" /><summary type="html">Mayx Forever Project – Phase II</summary></entry><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;'c = %r; print(c %% c)'&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'&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;'/home/mayx/blog-env'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;serviceWorker&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/sw.js&lt;/span&gt;&lt;span class=&quot;dl&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;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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Service Worker 注册成功:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Service Worker 注册失败:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;被反代的网站&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;dl&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;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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;activate&lt;/span&gt;&lt;span class=&quot;dl&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;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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;dl&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;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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Proxy error:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&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;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Proxy failed&lt;/span&gt;&lt;span class=&quot;dl&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;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></feed>