<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xml" href="https://mabbs.github.io/feed.xslt.xml"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mabbs.github.io/atom.xml" rel="self" type="application/atom+xml" /><link href="https://mabbs.github.io/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-06-09T15:44:18+08:00</updated><id>https://mabbs.github.io/atom.xml</id><title type="html">Mayx的博客</title><subtitle>Mayx&apos;s Home Page</subtitle><author><name>mayx</name></author><entry><title type="html">如何节约游戏占用的硬盘空间？</title><link href="https://mabbs.github.io/2026/06/01/dedupe.html" rel="alternate" type="text/html" title="如何节约游戏占用的硬盘空间？" /><published>2026-06-01T00:00:00+08:00</published><updated>2026-06-01T00:00:00+08:00</updated><id>https://mabbs.github.io/2026/06/01/dedupe</id><content type="html" xml:base="https://mabbs.github.io/2026/06/01/dedupe.html"><![CDATA[<p>浪费硬盘空间是可耻的！<!--more--></p>

<h1 id="起因">起因</h1>
<p>在几年前，我写过一篇在<a href="/2023/10/21/game.html">MacBook上玩游戏</a>的文章，在那之后，我已经在我的Mac上下载了几十部游戏。只不过有个问题……我的Mac只有256GiB的硬盘存储空间，下载一堆游戏会让我的硬盘空间不够用，但是又不太想删，所以我该怎么尽可能让游戏占用更少的空间呢？ <br />
  首先为了能在Mac上尽可能流畅地玩，我玩的游戏大多都是用跨平台能力很强的引擎编写的游戏，比如<a href="https://github.com/renpy/renpy">Ren’Py</a>、RPG制作大师、Godot之类的，而像RPG制作大师这种引擎制作的游戏还有一个特点，开发者一般都会使用引擎自带的素材进行开发，有时候还会用不少第三方的罐头素材之类的（实际上甚至还有好多AVG为了蹭这些引擎的公用素材刻意用它们），所以这几十个游戏里应该有非常多的重复素材，如果能想办法把它们去个重，应该能节省相当多的空间吧……</p>

<h1 id="去重的方法">去重的方法</h1>
<p>如果想要对文件进行去重，我搜了一下，有个叫做<a href="https://codeberg.org/jbruchon/jdupes">jdupes</a>的工具就很不错，它支持多种去重方式，比如使用硬链接，或者用一些文件系统的写时复制特性。不过如果用写时复制特性，jdupes在第二次执行的时候会认为去重后的文件还是单独的文件，就会重复去重了，而且最终也不好统计，反正对我玩的游戏来说，要去重的都是游戏素材，不存在后续修改的可能性，所以我打算全部用硬链接。 <br />
  所以最终要执行的命令也非常简单，直接一句<code class="language-plaintext highlighter-rouge">jdupes -r -L Game</code>就可以了，这样以后每次下载了新的游戏之后重复执行这个操作，就可以将游戏中和其他游戏里有的素材去重了。 <br />
  不过实际上很多游戏并不能直接用这种方式去重，因为它们的资源文件有些是打包成单个文件，有些进行了简单的加密，导致即使是相同的素材，文件也并不相同，所以我必须让所有的资源以单独原始的形态出现。对于不同的引擎也有不同的处理方式，所以接下来我需要对它们进行一些研究。</p>

<h1 id="不同引擎的处理方式">不同引擎的处理方式</h1>
<h2 id="rpg制作大师mvmz">RPG制作大师MV/MZ</h2>
<p>对于RPG制作大师MV/MZ开发的游戏来说，解密很简单，比较知名的是一个叫做<a href="https://gitlab.com/Petschko/RPG-Maker-MV-Decrypter">RPG-Maker-MV-Decrypter</a>的工具，它可以在浏览器中进行解密，但一个游戏的资源文件非常多……要是全上传给浏览器实在是太麻烦了……后来我又搜了一下，有一个用C#写的叫<a href="https://github.com/uuksu/RPGMakerDecrypter">RPG Maker Decrypter</a>工具也很不错，它作为命令行工具比在浏览器中执行简单多了，而且还能只把资源文件单独提出来，这样就可以剔除掉游戏自带的浏览器文件。不过他这个仓库的代码有个问题，它在选择文件的时候似乎会区分大小写，文件夹名中含有大写字母的似乎会被剔除……这样不太符合我的要求啊，当然我不会C#，于是我用AI改了一下，还给他提了个<a href="https://github.com/uuksu/RPGMakerDecrypter/pull/28">PR</a>，不过这家伙看起来似乎不太喜欢AI写的代码，看起来不打算合我的PR😅。不过无所谓了，反正我也是自用，他爱合不合吧。 <br />
  这个工具的用法也非常简单，一句<code class="language-plaintext highlighter-rouge">RPGMakerDecrypter-cli [input] -p -o [output]</code>就处理好了，处理完之后只需要把<code class="language-plaintext highlighter-rouge">data/System.json</code>中的<code class="language-plaintext highlighter-rouge">hasEncryptedImages</code>和<code class="language-plaintext highlighter-rouge">hasEncryptedAudio</code>设置为false就可以正常识别，以后在Mac中只要在游戏路径下执行<code class="language-plaintext highlighter-rouge">python3 -m http.server</code>就可以在浏览器中游玩了。 <br />
  在这个过程中，我还发现有一些游戏喜欢把原画文件直接放到游戏里面，一张图片好几M，但RPG制作大师的引擎在渲染的时候根本不会渲染出那么高的分辨率，结果毫无意义地浪费一大堆存储空间，而且因为图片是加密的，对大多数人来说也没有收藏价值。所以在解密完之后我就想干脆把这些图片全部有损压缩一遍，估计能节省不少存储空间，于是让AI写了个简单的压缩脚本处理了一下：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="s">"""
图片压缩脚本（多进程版本）
将 pictures.orig 文件夹中的图片使用 WebP 格式进行高效压缩，
保持分辨率不变，肉眼看不出差异，压缩后的图片保存到 pictures 文件夹。

使用方法:
    python3 compress_images.py

压缩策略:
    - 保持原始分辨率不变
    - 使用 WebP 格式（有损压缩，高质量）
    - 质量设置为 85，在保持视觉质量的同时显著减小文件大小
    - 文件名和后缀保持不变
    - 多进程并行处理
    - 处理失败时自动复制原文件
"""</span>

<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">shutil</span>
<span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">from</span> <span class="nn">multiprocessing</span> <span class="kn">import</span> <span class="n">Pool</span><span class="p">,</span> <span class="n">cpu_count</span>
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">partial</span>

<span class="c1"># 配置路径
</span><span class="n">SOURCE_DIR</span> <span class="o">=</span> <span class="s">"pictures.orig"</span>
<span class="n">OUTPUT_DIR</span> <span class="o">=</span> <span class="s">"pictures"</span>

<span class="c1"># WebP 质量设置 (0-100，数值越高质量越好，文件也越大)
# 85 是一个很好的平衡点，肉眼几乎看不出差异
</span><span class="n">WEBP_QUALITY</span> <span class="o">=</span> <span class="mi">85</span>

<span class="c1"># 对于带有透明通道的图片，可以设置不同的质量
</span><span class="n">WEBP_QUALITY_WITH_ALPHA</span> <span class="o">=</span> <span class="mi">80</span>

<span class="c1"># 并行进程数，默认为 CPU 核心数
</span><span class="n">NUM_WORKERS</span> <span class="o">=</span> <span class="n">cpu_count</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">compress_single_image</span><span class="p">(</span><span class="n">img_file</span><span class="p">:</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">bool</span><span class="p">,</span> <span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
    <span class="s">"""
    压缩单个图片文件（用于多进程）
    
    Args:
        img_file: (源文件路径, 输出文件路径, 输出目录) 元组
        
    Returns:
        (文件名, 是否成功, 原始大小, 压缩后大小) 元组
    """</span>
    <span class="n">source_path</span><span class="p">,</span> <span class="n">output_path_str</span><span class="p">,</span> <span class="n">output_dir</span> <span class="o">=</span> <span class="n">img_file</span>
    <span class="n">source_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">source_path</span><span class="p">)</span>
    <span class="n">output_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">output_path_str</span><span class="p">)</span>
    
    <span class="n">original_size</span> <span class="o">=</span> <span class="n">source_path</span><span class="p">.</span><span class="n">stat</span><span class="p">().</span><span class="n">st_size</span>
    
    <span class="k">try</span><span class="p">:</span>
        <span class="n">img</span> <span class="o">=</span> <span class="n">Image</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">source_path</span><span class="p">)</span>
        
        <span class="c1"># 检查是否有透明通道
</span>        <span class="n">has_alpha</span> <span class="o">=</span> <span class="n">img</span><span class="p">.</span><span class="n">mode</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'RGBA'</span><span class="p">,</span> <span class="s">'LA'</span><span class="p">,</span> <span class="s">'PA'</span><span class="p">)</span> <span class="ow">or</span> <span class="p">(</span><span class="n">img</span><span class="p">.</span><span class="n">mode</span> <span class="o">==</span> <span class="s">'P'</span> <span class="ow">and</span> <span class="s">'transparency'</span> <span class="ow">in</span> <span class="n">img</span><span class="p">.</span><span class="n">info</span><span class="p">)</span>
        
        <span class="c1"># 确定使用的质量
</span>        <span class="n">quality</span> <span class="o">=</span> <span class="n">WEBP_QUALITY_WITH_ALPHA</span> <span class="k">if</span> <span class="n">has_alpha</span> <span class="k">else</span> <span class="n">WEBP_QUALITY</span>
        
        <span class="c1"># 保存为 WebP 格式，但使用原始的文件扩展名
</span>        <span class="n">img</span><span class="p">.</span><span class="n">save</span><span class="p">(</span>
            <span class="nb">str</span><span class="p">(</span><span class="n">output_path</span><span class="p">),</span>
            <span class="nb">format</span><span class="o">=</span><span class="s">'WEBP'</span><span class="p">,</span>
            <span class="n">quality</span><span class="o">=</span><span class="n">quality</span><span class="p">,</span>
            <span class="n">method</span><span class="o">=</span><span class="mi">6</span>  <span class="c1"># 压缩方法 0-6，6 是最慢但压缩率最高的
</span>        <span class="p">)</span>
        
        <span class="n">compressed_size</span> <span class="o">=</span> <span class="n">output_path</span><span class="p">.</span><span class="n">stat</span><span class="p">().</span><span class="n">st_size</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">source_path</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="bp">True</span><span class="p">,</span> <span class="n">original_size</span><span class="p">,</span> <span class="n">compressed_size</span><span class="p">)</span>
        
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="c1"># 处理失败时，复制原文件到输出目录
</span>        <span class="k">try</span><span class="p">:</span>
            <span class="n">shutil</span><span class="p">.</span><span class="n">copy2</span><span class="p">(</span><span class="n">source_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span>
            <span class="n">compressed_size</span> <span class="o">=</span> <span class="n">output_path</span><span class="p">.</span><span class="n">stat</span><span class="p">().</span><span class="n">st_size</span>
            <span class="k">return</span> <span class="p">(</span><span class="n">source_path</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="bp">False</span><span class="p">,</span> <span class="n">original_size</span><span class="p">,</span> <span class="n">compressed_size</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">copy_error</span><span class="p">:</span>
            <span class="k">return</span> <span class="p">(</span><span class="n">source_path</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="bp">False</span><span class="p">,</span> <span class="n">original_size</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">source_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">SOURCE_DIR</span><span class="p">)</span>
    <span class="n">output_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">OUTPUT_DIR</span><span class="p">)</span>
    
    <span class="c1"># 检查源目录是否存在
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="n">source_dir</span><span class="p">.</span><span class="n">exists</span><span class="p">():</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"错误: 源目录 '</span><span class="si">{</span><span class="n">SOURCE_DIR</span><span class="si">}</span><span class="s">' 不存在"</span><span class="p">)</span>
        <span class="k">return</span>
    
    <span class="c1"># 创建输出目录
</span>    <span class="n">output_dir</span><span class="p">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    
    <span class="c1"># 获取所有图片文件（支持多种格式）
</span>    <span class="n">image_extensions</span> <span class="o">=</span> <span class="p">(</span><span class="s">'*.png'</span><span class="p">,</span> <span class="s">'*.jpg'</span><span class="p">,</span> <span class="s">'*.jpeg'</span><span class="p">,</span> <span class="s">'*.bmp'</span><span class="p">,</span> <span class="s">'*.gif'</span><span class="p">,</span> <span class="s">'*.tiff'</span><span class="p">,</span> <span class="s">'*.webp'</span><span class="p">)</span>
    <span class="n">image_files</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">ext</span> <span class="ow">in</span> <span class="n">image_extensions</span><span class="p">:</span>
        <span class="n">image_files</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">source_dir</span><span class="p">.</span><span class="n">glob</span><span class="p">(</span><span class="n">ext</span><span class="p">))</span>
    <span class="n">image_files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">image_files</span><span class="p">))</span>  <span class="c1"># 去重并排序
</span>    
    <span class="k">if</span> <span class="ow">not</span> <span class="n">image_files</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"在 '</span><span class="si">{</span><span class="n">SOURCE_DIR</span><span class="si">}</span><span class="s">' 中没有找到图片文件"</span><span class="p">)</span>
        <span class="k">return</span>
    
    <span class="c1"># 构建任务列表
</span>    <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">img_file</span> <span class="ow">in</span> <span class="n">image_files</span><span class="p">:</span>
        <span class="n">output_path</span> <span class="o">=</span> <span class="n">output_dir</span> <span class="o">/</span> <span class="n">img_file</span><span class="p">.</span><span class="n">name</span>  <span class="c1"># 保持原文件名和后缀
</span>        <span class="n">tasks</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="nb">str</span><span class="p">(</span><span class="n">img_file</span><span class="p">),</span> <span class="nb">str</span><span class="p">(</span><span class="n">output_path</span><span class="p">),</span> <span class="nb">str</span><span class="p">(</span><span class="n">output_dir</span><span class="p">)))</span>
    
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"找到 </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span><span class="si">}</span><span class="s"> 个图片文件"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"源目录: </span><span class="si">{</span><span class="n">SOURCE_DIR</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"输出目录: </span><span class="si">{</span><span class="n">OUTPUT_DIR</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"WebP 质量设置: </span><span class="si">{</span><span class="n">WEBP_QUALITY</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"并行进程数: </span><span class="si">{</span><span class="n">NUM_WORKERS</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"-"</span> <span class="o">*</span> <span class="mi">70</span><span class="p">)</span>
    
    <span class="c1"># 使用多进程池处理图片
</span>    <span class="n">success_count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">fail_count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">total_original</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">total_compressed</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="k">with</span> <span class="n">Pool</span><span class="p">(</span><span class="n">processes</span><span class="o">=</span><span class="n">NUM_WORKERS</span><span class="p">)</span> <span class="k">as</span> <span class="n">pool</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">success</span><span class="p">,</span> <span class="n">original_size</span><span class="p">,</span> <span class="n">compressed_size</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">pool</span><span class="p">.</span><span class="n">imap</span><span class="p">(</span><span class="n">compress_single_image</span><span class="p">,</span> <span class="n">tasks</span><span class="p">),</span> <span class="mi">1</span><span class="p">):</span>
            <span class="n">total_original</span> <span class="o">+=</span> <span class="n">original_size</span>
            <span class="n">total_compressed</span> <span class="o">+=</span> <span class="n">compressed_size</span>
            
            <span class="k">if</span> <span class="n">success</span><span class="p">:</span>
                <span class="n">success_count</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="n">marker</span> <span class="o">=</span> <span class="s">"✓"</span>
                <span class="n">reduction</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">compressed_size</span> <span class="o">/</span> <span class="n">original_size</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">if</span> <span class="n">original_size</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="mi">0</span>
                <span class="n">status_msg</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">reduction</span><span class="si">:</span><span class="o">+</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">%"</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">fail_count</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="n">marker</span> <span class="o">=</span> <span class="s">"✗"</span>
                <span class="n">status_msg</span> <span class="o">=</span> <span class="s">"复制原文件"</span>
            
            <span class="n">status</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"[</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span><span class="si">}</span><span class="s">] </span><span class="si">{</span><span class="n">filename</span><span class="si">}</span><span class="s">"</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">marker</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">status</span><span class="si">:</span><span class="mi">50</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">original_size</span><span class="o">/</span><span class="mi">1024</span><span class="si">:</span><span class="o">&gt;</span><span class="mf">8.1</span><span class="n">f</span><span class="si">}</span><span class="s">KB -&gt; </span><span class="si">{</span><span class="n">compressed_size</span><span class="o">/</span><span class="mi">1024</span><span class="si">:</span><span class="o">&gt;</span><span class="mf">8.1</span><span class="n">f</span><span class="si">}</span><span class="s">KB (</span><span class="si">{</span><span class="n">status_msg</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    
    <span class="c1"># 输出总结
</span>    <span class="k">print</span><span class="p">(</span><span class="s">"-"</span> <span class="o">*</span> <span class="mi">70</span><span class="p">)</span>
    <span class="n">total_reduction</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">total_compressed</span> <span class="o">/</span> <span class="n">total_original</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">if</span> <span class="n">total_original</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="mi">0</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"压缩完成!"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  成功处理: </span><span class="si">{</span><span class="n">success_count</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span><span class="si">}</span><span class="s"> 个文件"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">fail_count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  失败(已复制原文件): </span><span class="si">{</span><span class="n">fail_count</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span><span class="si">}</span><span class="s"> 个文件"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  原始总大小: </span><span class="si">{</span><span class="n">total_original</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s"> MB (</span><span class="si">{</span><span class="n">total_original</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> KB)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  压缩后大小: </span><span class="si">{</span><span class="n">total_compressed</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s"> MB (</span><span class="si">{</span><span class="n">total_compressed</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> KB)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  总压缩率: </span><span class="si">{</span><span class="n">total_reduction</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">%"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  节省空间: </span><span class="si">{</span><span class="p">(</span><span class="n">total_original</span> <span class="o">-</span> <span class="n">total_compressed</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s"> MB"</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>
<p>最终压缩完之后我把原图上传到了<a href="https://e-hentai.org/g/3901673/426a7a17ba/">EH画廊</a>中，本地只留压缩后的图片，大小从原来的2GiB多下降到了300多MiB，可以说效果相当显著了。 <br />
  除此之外还有一些游戏使用了Ogg FLAC背景音乐，这种音乐不仅占用磁盘空间很大，而且我在Safari上玩的时候浏览器根本没法解析（Chrome应该可以）。虽然我听音乐是会考虑<a href="/2025/03/22/hifi.html">HiFi</a>，但玩游戏就没必要了吧……所以像这种音乐，就得用一句：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> input.flac.ogg <span class="nt">-c</span>:a vorbis <span class="nt">-strict</span> <span class="nt">-2</span> <span class="nt">-q</span>:a 10 output.ogg
</code></pre></div></div>
<p>转换为正常有损的Ogg音乐了。</p>
<h2 id="rpg制作大师xpvxva">RPG制作大师XP/VX/VA</h2>
<p>对于RPG制作大师XP/VX/VA引擎开发的游戏来说，它们都是基于用Ruby语言开发的RGSS编写的，作为脚本来说，倒是有跨平台的条件，但因为官方并没有做跨平台，所以不能直接在Mac上运行。不过有一款叫做<a href="https://github.com/mkxp-z/mkxp-z">mkxp-z</a>的工具允许跨平台运行使用RPG制作大师XP/VX/VA制作的游戏，因此这类游戏我也收集了一些。 <br />
  这些游戏的资源通常会进行简单的混淆加密，一般会打包成单个RGSSAD文件，这个解包也很简单，用刚刚的RPG Maker Decrypter就可以。不过这种游戏还有个特点，有些游戏需要使用<a href="https://www.rpgmakerweb.com/run-time-package">RTP</a>才能运行，它这个RTP其实就是RPG制作大师自带的素材包，当时设计出来估计也是想着用来节约硬盘空间吧，就是不知道为什么到后来的MV/MZ却取消了这种方式……虽然mkxp-z是支持通过配置文件引入RTP的，但既然我已经选择了硬链接的方式，就没必要单独搞RTP了，我选择把RTP直接和游戏合并，然后让jdupes直接去重就好了，这样相比于RTP的方式还有一些好处就是XP/VX/VA可能有一些和MV/MZ使用相同的素材，这部分也可以不用占用重复的空间了。</p>
<h2 id="renpy">Ren’Py</h2>
<p>对于Ren’Py来说，因为这个引擎并没有自带的公共资源，所以重复素材的问题并不是很大。不过在我之前对<a href="/2024/01/20/renpy.html">Ren’Py的探索</a>中提到过，我玩的一些游戏是系列游戏，这种系列游戏有非常多的素材复用，但显然开发者并不会为了节约玩家硬盘空间而共享这部分资源，而且Ren’Py游戏也都是打包成单个文件的，所以接下来我们依然得要解包才能进行去重处理。 <br />
  Ren’Py使用的rpa文件解包起来依然很简单，有一款现成的工具<a href="https://github.com/Lattyware/unrpa">unrpa</a>可以直接解包，用pip就能安装。不知道为什么这些引擎总是喜欢把资源文件都打成一个包，明明很容易就能解包……难道是为了性能吗？ <br />
  不过也正是因为Ren’Py的公共资源不多，如果玩的不是系列游戏，就没有解包的必要了，解包之后一堆小文件有可能会比整个rpa文件更大，毕竟文件系统存在“簇”，有可能会消耗没对齐的空间。</p>

<h1 id="验证结果">验证结果</h1>
<p>最终进行完上述操作，可以通过执行<code class="language-plaintext highlighter-rouge">du -sh</code>和<code class="language-plaintext highlighter-rouge">du -shl</code>进行对比来验证节约的硬盘空间，我在这次游戏的瘦身中节约了：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ % du -sh Game                                
 33G	Game
~ % du -shl Game
 47G	Game
</code></pre></div></div>
<p>看起来还是相当可观啊……尤其是在当下硬盘价格大涨的情况下，如果很多人能通过这些方式来节约硬盘空间，就能减少对硬盘容量的需求吧……不过说到底其实也都是网上能下到的资源，也许玩完之后就删掉才是最好的节约硬盘的方式吧😂。</p>

<p><input name="live2dBGM" value="https://music.163.com/song/media/outer/url?id=1968116350.mp3" type="hidden" /></p>]]></content><author><name>mayx</name></author><category term="dedupe" /><category term="RPG制作大师" /><category term="游戏" /><summary type="html"><![CDATA[浪费硬盘空间是可耻的！]]></summary></entry><entry><title type="html">虚拟局域网的组网探索记录</title><link href="https://mabbs.github.io/2026/05/01/virtual-net.html" rel="alternate" type="text/html" title="虚拟局域网的组网探索记录" /><published>2026-05-01T00:00:00+08:00</published><updated>2026-05-01T00:00:00+08:00</updated><id>https://mabbs.github.io/2026/05/01/virtual-net</id><content type="html" xml:base="https://mabbs.github.io/2026/05/01/virtual-net.html"><![CDATA[<p>异地组网，有多少种选择？<!--more--></p>

<h1 id="起因">起因</h1>
<p>最近我有一些放置在许多不同地方的机器，有一些东西需要让它们之间能够相互访问。虽然我很久以前写过一篇使用<a href="/2021/05/07/ssh.html">SSH进行互联</a>的文章，但这样做每个服务都需要单独配置，也不方便管理。所以为了能让机器之间能够轻松通信，我打算组建一个虚拟局域网，让它们像在同一交换机下一样。不过这种组网的工具非常多，我应该选哪个比较好呢？</p>

<h1 id="不同组网工具的体验">不同组网工具的体验</h1>
<h2 id="n2n">n2n</h2>
<p>以前我用过一款用C写的叫做<a href="https://github.com/ntop/n2n">n2n</a>的工具，它可以很轻松地组建一个P2P的二层虚拟网络，而且生态也不错，手机、电脑、路由器、服务器上都有可以用的客户端。使用起来非常简单，它的中继和穿透服务程序叫做Supernode，无需太多的配置，只要在有公网的服务器安装并使用<code class="language-plaintext highlighter-rouge">-p</code>指定一个端口就可以启动。而客户端配置也非常简单，用<code class="language-plaintext highlighter-rouge">-l</code>配置好Supernode的地址，然后让想要在同一个网络的机器使用相同的任意<code class="language-plaintext highlighter-rouge">-k</code>和<code class="language-plaintext highlighter-rouge">-c</code>就可以成功组网，可以说算是非常好用了。 <br />
  唯一的问题就是它这个项目看起来似乎已经停止更新了……虽然大多数情况下用起来没问题，但是有时候还是会出现组网不太可靠的情况。如果两个机器都不经过NAT，可以通过公网IP连接，它的可靠性还可以。但如果是两个NAT后的机器之间，有时候会存在莫名掉线的情况，也许是因为穿透导致的不可靠？总之遇到这种情况之后重启又能正常工作，说明是软件本身的问题，但它停更了……所以对我来说它的可靠性不太够。（其实它还有个叫做<a href="https://github.com/n42n/n3n">n3n</a>的继任者，不过知名度不高，所以生态也不太行）</p>
<h2 id="wireguard">WireGuard</h2>
<p>其实在这之后我本来是打算用L2TP/IPSec进行组网的，但看了一下貌似配置有点复杂，而且不够现代，现在想要组网貌似大多都推荐<a href="https://git.zx2c4.com/wireguard-linux/">WireGuard</a>作为更现代的选择。只不过它和n2n相比来说是三层的虚拟网络，如果需要发送非TCP/IP协议的特别包，可能就用不了它吧，当然对我来说没有这种需求。它用起来也非常简单，不过正常情况下它设计是为了点对点传输，而且没有自带的NAT穿透功能，所以如果想要实现组网，就得搭一个星形网络，让互联网上的服务器作为虚拟的交换机，这个做起来倒也不复杂。首先，每个节点需要生成一个公私钥对作为身份证明，在安装好WireGuard之后执行<code class="language-plaintext highlighter-rouge">wg genkey</code>就能生成私钥。作为交换机的节点需要在<code class="language-plaintext highlighter-rouge">/etc/wireguard/wg0.conf</code>中写一个这样的配置：</p>
<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[<span class="n">Interface</span>]
<span class="n">PrivateKey</span> = <span class="n">xxx</span>
<span class="n">Address</span> = <span class="m">192</span>.<span class="m">168</span>.<span class="m">1</span>.<span class="m">1</span>/<span class="m">24</span>
<span class="n">ListenPort</span> = <span class="m">51820</span>

<span class="n">PostUp</span> = <span class="n">iptables</span> -<span class="n">A</span> <span class="n">FORWARD</span> -<span class="n">i</span> <span class="n">wg0</span> -<span class="n">o</span> <span class="n">wg0</span> -<span class="n">j</span> <span class="n">ACCEPT</span>
<span class="n">PostDown</span> = <span class="n">iptables</span> -<span class="n">D</span> <span class="n">FORWARD</span> -<span class="n">i</span> <span class="n">wg0</span> -<span class="n">o</span> <span class="n">wg0</span> -<span class="n">j</span> <span class="n">ACCEPT</span>

<span class="c"># 机器1
</span>[<span class="n">Peer</span>]
<span class="n">PublicKey</span> = <span class="n">xxx</span>
<span class="n">AllowedIPs</span> = <span class="m">192</span>.<span class="m">168</span>.<span class="m">1</span>.<span class="m">2</span>/<span class="m">32</span>

<span class="c"># 机器2
</span>[<span class="n">Peer</span>]
<span class="n">PublicKey</span> = <span class="n">xxx</span>
<span class="n">AllowedIPs</span> = <span class="m">192</span>.<span class="m">168</span>.<span class="m">1</span>.<span class="m">3</span>/<span class="m">32</span>
</code></pre></div></div>
<p>其中PrivateKey填写交换机自己的私钥，而作为使用者的Peer中的PublicKey可以用对应节点的私钥执行<code class="language-plaintext highlighter-rouge">echo xxx | wg pubkey</code>这个命令查看，然后每个Peer需要像这样配置：</p>
<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[<span class="n">Interface</span>]
<span class="n">PrivateKey</span> = <span class="n">xxx</span>
<span class="n">Address</span> = <span class="m">192</span>.<span class="m">168</span>.<span class="m">1</span>.<span class="m">2</span>/<span class="m">24</span>

[<span class="n">Peer</span>]
<span class="n">PublicKey</span> = <span class="n">xxx</span> <span class="c"># 交换机节点的公钥
</span><span class="n">Endpoint</span> = <span class="n">xxx</span>.<span class="n">xxx</span>.<span class="n">xxx</span>.<span class="n">xxx</span>:<span class="m">51820</span> <span class="c"># 交换机节点的地址
</span><span class="n">AllowedIPs</span> = <span class="m">192</span>.<span class="m">168</span>.<span class="m">1</span>.<span class="m">0</span>/<span class="m">24</span>
<span class="n">PersistentKeepalive</span> = <span class="m">25</span>
</code></pre></div></div>
<p>最后全都配置好之后所有节点使用<code class="language-plaintext highlighter-rouge">systemctl enable --now wg-quick@wg0</code>启动就可以了，启动之后每个节点可以执行<code class="language-plaintext highlighter-rouge">wg</code>查看当前的连接状态。 <br />
  当然这是在Linux上，至于其他系统大多都有GUI配置，填起来更简单。它的生态也非常好，基本上常见的操作系统都支持，具体可以在<a href="https://www.wireguard.com/install/">官网</a>查看支持的系统和安装方法。不过由于它在Linux中优先使用内核模块，导致我在一些比较小众的环境中也是遇到了各种特别的问题。</p>
<h3 id="在红米ax3000中遇到的问题">在红米AX3000中遇到的问题</h3>
<p>我在这个网络中有几个安装了OpenWrt的路由器，在这其中使用联发科芯片的路由器基本上都没什么问题，官网能轻松下载到固件，也能很轻松地在软件包中找到WireGuard并安装，但我还有一台使用高通芯片的红米AX3000，似乎因为高通对资料管控得很严格，导致它没有官网的固件，最终我在GitHub上找了一个其他人自己编译的<a href="https://github.com/hzyitc/openwrt-redmi-ax3000/">固件</a>。虽然它整起来有点麻烦，不过倒也能用，但是在我尝试安装WireGuard的时候遇到了麻烦…… <br />
  它的软件包里有WireGuard，也能找到对应的内核模块安装包，但安装完之后没法启动……随后我看了一下它下载的<a href="https://github.com/hzyitc/openwrt-redmi-ax3000/blob/gh-pages/ipq50xx-qsdk-kernel-5.4-openwrt-21.02-qsdk-11.5.05.841.1029/ci-20240727-173350-ab1f9ffa/kmod-wireguard_5.4-qsdk-11.5.0.5-1_arm_cortex-a7_neon-vfpv4.ipk">安装包</a>，结果发现是空的😰，它这个固件的内核模块可能是在编译的时候遇到了一些问题。至于让我自己编译这个内核模块，难度似乎有点高了……那怎么办呢？要知道Linux的内核模块都是和内核挂钩的，没办法随便找一个别的模块使用。还好WireGuard倒也不止有内核模块，也有一些在用户空间中的实现，比如<a href="https://git.zx2c4.com/wireguard-go">wireguard-go</a>和<a href="https://git.zx2c4.com/wireguard-rs">wireguard-rs</a>。只是官方似乎非常不推荐在Linux上使用它们，所以没有提供预编译的版本。不过遇到这种问题的人也许是比较多，所以有人做了在<a href="https://github.com/seud0nym/openwrt-wireguard-go">OpenWrt上使用的wireguard-go</a>，安装好之后效果和使用内核模块的感觉基本上没什么区别，最终也能连通，唯一的区别就是在执行<code class="language-plaintext highlighter-rouge">wg</code>的时候，会显示“Interface: wg0 (userspace)”罢了。从效率上来说虽然肯定没有内核模块那么高，但它其实也用了“Tun”模块，理论上和使用“Tap”模块的n2n应该差不多吧。</p>
<h3 id="在openeuler中遇到的问题">在openEuler中遇到的问题</h3>
<p>在我使用的节点中，还有一台安装了openEuler 22.03 LTS操作系统的服务器，虽然openEuler和CentOS可以说基本上没什么区别，但毕竟它的内核是openEuler自己编译的，所以没办法直接使用CentOS的内核模块。并且openEuler的源中也完全没有提供和WireGuard相关的包，所以想要在openEuler上安装WireGuard还是有些挑战（当然如果觉得麻烦，它们倒是有一个兼容WireGuard的客户端<a href="https://eur.openeuler.openatom.cn/coprs/nucleo/tunsafe/">TunSafe</a>可以凑活用一下）。 <br />
  后来我试了一下在这上面安装wireguard-tools倒是可以直接用<a href="https://mirrors.tuna.tsinghua.edu.cn/epel/8/Everything/x86_64/Packages/w/wireguard-tools-1.0.20210914-1.el8.x86_64.rpm">CentOS 8EPEL源中的包</a>，但openEuler的内核在编译的时候故意没有包含WireGuard内核模块……这该怎么办呢？用wireguard-go吗？虽然这样可以很简单地解决，但感觉这样就是认输了😂。后来我搜了一下，找到了一篇<a href="https://dingle.site/archives/wei-openeulertian-jia-wireguardmo-kuai">在openEuler安装WireGuard内核模块</a>的文章，方法大致如下：</p>
<ol>
  <li>首先安装编译环境和源代码。
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yum <span class="nb">install </span>elfutils-libelf-devel kernel-devel pkgconfig <span class="s2">"@Development Tools"</span>
yum <span class="nb">install </span>kernel-headers.x86_64 pkg-config ncurses-devel openssl-devel dwarves
yum <span class="nb">install </span>kernel-source.x86_64
</code></pre></div>    </div>
  </li>
  <li>然后进行编译配置，内核源码一般会安装到<code class="language-plaintext highlighter-rouge">/usr/src/</code>下，找到之后在里面执行<code class="language-plaintext highlighter-rouge">make menuconfig</code>，然后勾选“Device Drivers -&gt; Network device support -&gt; Wireguard secure network tunnel”并保存。</li>
  <li>最后执行<code class="language-plaintext highlighter-rouge">make</code>开始编译，为了加速可以用<code class="language-plaintext highlighter-rouge">-j</code>参数加上CPU的核心数进行并行编译，当时编译就花掉了一整天😂，理论上应该可以只编译WireGuard和它依赖的几个模块，不过我不太清楚怎么做，还是费点时间按照文中说的做吧。</li>
  <li>执行<code class="language-plaintext highlighter-rouge">make modules_install</code>将编译好的结果安装到<code class="language-plaintext highlighter-rouge">/lib/modules/5.10.0</code>。
  不过系统似乎不会去这个路径下找内核模块，所以还得把这里面的kernel文件夹复制到<code class="language-plaintext highlighter-rouge">/lib/modules/$(uname -r)</code>下，然后执行<code class="language-plaintext highlighter-rouge">depmod -a</code>更新模块依赖。</li>
  <li>最后执行<code class="language-plaintext highlighter-rouge">modprobe wireguard</code>验证模块是否能正常加载，如果没有报错并且可以在<code class="language-plaintext highlighter-rouge">lsmod | grep wireguard</code>中看到，就说明安装成功了，剩余的步骤和其他Linux系统一样。</li>
</ol>

<h3 id="wireguard的控制平面">WireGuard的控制平面</h3>
<p>虽然WireGuard本身配置很简单，但每加一个节点还得在交换机节点上修改一下配置文件，稍微有些麻烦，所以有人开发了一些控制平面，让它可以被更规范地管理，比如<a href="https://github.com/gravitl/netmaker">Netmaker</a>和<a href="https://github.com/juanfont/headscale">Headscale</a>。而Headscale主要是为Tailscale客户端开发的开源服务器端，因此功能会局限于Tailscale提供的功能。所以如果没有用过Tailscale，可以优先考虑Netmaker。 <br />
  这两个控制平面支持的功能相当丰富，而且它们还支持让WireGuard进行NAT穿透，自动组建Mesh网络，不像我一堆在NAT后的设备还要直接使用WireGuard就只能搭成星形网络。只不过对我来说，我也用不到那么多企业级功能，这个服务端配置起来也有点麻烦，而且我也没有很多节点需要动态增减，我的云端服务器带宽也足够使用，所以就没有用这些东西了😆。</p>
<h2 id="其他的组网工具">其他的组网工具</h2>
<p>除了WireGuard之外，还有很多其他的组网工具，比如<a href="https://github.com/vnt-dev/vnt">VNT</a>和<a href="https://github.com/EasyTier/Easytier">EasyTier</a>，这俩用起来也非常简单，只需要加几个参数就能组网，和n2n一样。不过功能相比于n2n来说要强大不少，也支持NAT穿透，而且还都兼容WireGuard协议，另外不像WireGuard强制使用UDP传输，这两个还能用TCP和WebSocket，在特殊网络环境下应该比直接用WireGuard更好。另外它们都是Rust编写的，也许会更安全😋？可惜我已经配好WireGuard之后懒得再改了，如果以后有机会，可以尝试一下。</p>

<h1 id="总结">总结</h1>
<p>现在如果想要异地搭建虚拟局域网，还是有相当多的选择，而且无论是性能还是配置难度，都比以前好了不少。看来这种需求还是相当多啊，也正是因为有这些需求，所以才会出现这么多的方案可以用吧……总之我最后还是选择了纯WireGuard方案，主要还是简单够用，可靠性也不错，而且折腾了这么多再换也不太合适吧🤣。</p>]]></content><author><name>mayx</name></author><category term="虚拟网络" /><category term="异地组网" /><category term="WireGuard" /><summary type="html"><![CDATA[异地组网，有多少种选择？]]></summary></entry><entry><title type="html">关于AI个人助理的探索</title><link href="https://mabbs.github.io/2026/04/14/ai-agent.html" rel="alternate" type="text/html" title="关于AI个人助理的探索" /><published>2026-04-14T00:00:00+08:00</published><updated>2026-04-14T00:00:00+08:00</updated><id>https://mabbs.github.io/2026/04/14/ai-agent</id><content type="html" xml:base="https://mabbs.github.io/2026/04/14/ai-agent.html"><![CDATA[<p>给AI添加手脚能有多少种方法？<!--more--></p>

<h1 id="起因">起因</h1>
<p>最近像<a href="https://github.com/openclaw/openclaw">OpenClaw</a>这样被叫做“AI个人助理”的Agent越来越火了，当然这种东西在我看来依然是新瓶装旧酒，整来整去还就是和AutoGPT一样。这种东西在当时GPT-3.5的时候就有了，现在只不过是增加了聊天软件交互的渠道便突然大火，和当年的Manus一样……实在是无法理解。 <br />
  一年前我用过Devin.ai这个云端的Agent编写过<a href="/2025/04/08/feed.html">用JS解析订阅源</a>的脚本，体验还算不错，既然现在已经过了一年，那就让我看看现在又有了什么样的发展吧。</p>

<h1 id="使用ai个人助理">使用AI个人助理</h1>
<h2 id="体验原生openclaw">体验原生OpenClaw</h2>
<p>虽然感觉OpenClaw对我的意义不大，但我还是安装体验了一下。不过在国内安装它还是相对有点困难，毕竟国内无论是访问GitHub，还是NPM都有点麻烦，而且还需要有LLM提供商的信息……安装好之后使用起来感觉问题也非常多，经常出现执行一半就停止执行，在它执行的过程中看到它的操作不正确的时候也不能发言打断，而且很多时候最终任务执行的效果也不太好，这也可能是我用的国产开源模型推理能力有限，没舍得用Claude之类先进模型的锅😂？ <br />
  另外我也尝试让它加入MoltBook、MomoClaw、InStreet、百度贴吧抓虾吧之类的AI社区让它帮我宣传我的博客，但效果也很差，它每次发的时候会忘掉之前发的内容，结果就是同一篇内容发了好几遍……不过在这期间，有个叫PushMeBot的家伙在<a href="https://www.moltbook.com/post/7f1b0e1f-5175-4fd1-ad78-856be8b66250">Moltbook的帖子</a>中让我的OpenClaw执行一个网络监视程序，最终安装好之后给我<a href="https://basescan.org/tx/0x44dbfe53f276201447f3877bf050a5d56adebf5fe05235264ee665da717e9373">发了9USDC</a>😝，还挺有意思。 <br />
  总之按照我的体验，实在是想不出它能火的理由，体验不算很好，而且还要安装Node环境，完全不像是能让大众轻松使用的东西。 <br />
  不过这个项目似乎本身就是Vibe Coding的产物，体验不好也能理解，就看火了之后能有多少人完善它吧。</p>
<h2 id="国内大厂的二开claw">国内大厂的二开Claw</h2>
<p>国内好多大厂倒是看中了这个东西的爆火，像腾讯就出了几款这样的软件，比如QClaw。它可以不需要配置额外的环境，能像传统的软件一样直接安装使用，而且有自带的模型，有一定的免费额度可以用。配置技能也比较简单，直接点击就可以完成。而且可以直接扫码关联微信，直接通过微信和它进行交流，可以说是相当的傻瓜化了。不过QClaw给的免费额度虽然用来聊天之类的没问题，但对于开发软件还是有点少，所以他们还出了个叫做WorkBuddy的软件，它送的初始额度比QClaw要多不少，所以更适合用来开发。只不过为啥腾讯要出两个功能一样的软件？看起来应该是不同团队出的，可能是面向的用户群体不一样，所以搞了两套吧？</p>
<h2 id="vscode中的agent">VSCode中的Agent</h2>
<p>但要说开发的话，用作为“AI个人助理”的某些Claw其实并不合适，毕竟正常开发还是以人开发为主，全AI开发总会有些问题，所以开发的时候还是用编辑器集成的AI比较好。在三年前我就在用<a href="/2023/04/05/ai.html">GitHub Copilot</a>了，到现在我依然在用。现在的Copilot已经支持了Agent功能，开发相比之前也是强了很多，只不过现在的我没有学生身份，Copilot Free偶尔也会出现不够用的情况。不过对于Agent这类功能实现起来还是太简单了，所以有人开发这种功能的插件也很正常，比如<a href="https://github.com/cline/cline">Cline</a>，Copilot只能用微软提供的几个模型，而Cline可以自定义模型，用起来也很方便。</p>
<h2 id="微型开发板上运行的claw">微型开发板上运行的Claw</h2>
<p>前段时间，我闲来无事看了一下两年前买的<a href="/2024/02/24/luckfox.html">Luckfox Pico Plus</a>开发板的文档，偶然发现了一个很有意思的项目，叫做<a href="https://github.com/LuckfoxTECH/luckclaw">LuckClaw</a>，这是一个基于<a href="https://github.com/HKUDS/nanobot">nanobot</a>用Golang重构的轻量个人AI助手，可以在仅仅64MiB内存的超有限环境下运行一个和OpenClaw功能几乎相当的AI个人助理，真的是非常厉害。 <br />
  我在我的开发板上试了一下，体验很不错，安装不需要额外环境，直接下载就能使用，Go语言的程序确实方便。配置也很简单，直接执行<code class="language-plaintext highlighter-rouge">luckclaw config</code>就可以交互式进行模型等设置的配置，而且作为国产的应用，它也能很方便的对接国内聊天软件。只是限于开发板本身的能力，浏览器功能自然无法使用，所以搜索如果不借助那些需要API Key的AI专用接口，就基本上不能用……但总的来说效果已经非常不错了，至少有那些Claw的80%能力。 <br />
  （2025.04.15补充：后来我发现这种超精简的Claw项目看起来还挺多，比如<a href="https://github.com/zeroclaw-labs/zeroclaw">ZeroClaw</a>和<a href="https://github.com/sipeed/picoclaw">PicoClaw</a>，甚至还有给单片机用的<a href="https://github.com/memovai/mimiclaw">MimiClaw</a>。而且有意思的是，PicoClaw是Luckfox的竞争对手开发的，但是LuckClaw中却包含PicoClaw字样的注释，结果功能也没PicoClaw强，关注度也更低，属于是没抄明白了🤣） <br />
  想到前段时间还有人为了OpenClaw专门买Mac Mini，就感觉很有意思😆，这个东西看起来应该是在路由器上都能跑。所以想要AI个人助理，硬件完全不是问题，只要整一个能24小时挂机的东西，就可以满足绝大多数人的需求了。</p>
<h2 id="在手机上运行的claw">在手机上运行的Claw</h2>
<p>其实很多人也有比开发板和路由器性能更强的闲置设备，那就是手机，所以有人开发了一款叫做<a href="https://github.com/apkclaw-team/ApkClaw">ApkClaw</a>的软件，一样可以接入国内聊天软件。它既然能在手机上运行，当然和在其他平台运行的Claw相比有一个独特的优势，那就是操作手机应用。现在手机的应用相比电脑应用对于很多普通人来说功能更强大，所以它能做的事情可能比其他的Claw还多。我试了一下，配置也很方便，只不过能配置的项目太少了，看起来似乎没有安装Skill之类的功能，也许是因为它是相对早期的软件，所以功能还比较少吧。</p>

<h1 id="感想">感想</h1>
<p>总的来说，现在的Agent依然没有非常明显的进步，问题依旧很多，只是化身“AI个人助理”之后，增加了不少应用场景。这倒也是好事，在广泛传播的过程中，也能让很多对技术了解不多，但是很有想法的人参与其中，也许能对AI的应用化增添不少力量吧。</p>]]></content><author><name>mayx</name></author><category term="AI" /><category term="Agent" /><category term="个人助理" /><summary type="html"><![CDATA[给AI添加手脚能有多少种方法？]]></summary></entry><entry><title type="html">近期LLM的部署与应用经历(3)</title><link href="https://mabbs.github.io/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>https://mabbs.github.io/2026/03/01/llm3</id><content type="html" xml:base="https://mabbs.github.io/2026/03/01/llm3.html"><![CDATA[<p>用更多的方式探索AI！<!--more--></p>

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

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

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

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

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

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

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

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

<h1 id="感想">感想</h1>
<p>虽然有一些技术会因为各种各样的原因消失，但这不代表我们就要妥协一些东西，总有一些不同的技术可以解决相同的问题，所以我们只需要用其他的技术去实现就好了。不过这也是没办法的事情，毕竟没人能改变浏览器厂商们的决策啊😂。</p>]]></content><author><name>mayx</name></author><category term="XML" /><category term="Feed" /><category term="XSLT" /><category term="美化" /><summary type="html"><![CDATA[即使没有了XSLT，也不能让读者看到光秃秃的XML！]]></summary></entry><entry><title type="html">年终总结</title><link href="https://mabbs.github.io/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>https://mabbs.github.io/2026/01/01/summary</id><content type="html" xml:base="https://mabbs.github.io/2026/01/01/summary.html"><![CDATA[<p>0 error(s), ∞ warning(s)<!--more--></p>

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

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

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

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

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

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

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

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

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

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

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

<h1 id="感想">感想</h1>
<p>不知道用“量”来确保博客的永恒更可靠……还是用“质”的方式更好呢？其实我觉得还得是活动的更好，就像我以前所说的，如果有<a href="/2024/11/02/trojan.html#%E6%84%9F%E6%83%B3">僵尸网络</a>，自动帮我执行发现并推送的操作，也许比等着这些实例逐渐消失更好吧……只不过那样可能就不太友好了😂。</p>]]></content><author><name>mayx</name></author><category term="Git" /><category term="Gitea" /><category term="镜像" /><category term="Forever" /><summary type="html"><![CDATA[Mayx Forever Project – Phase II]]></summary></entry><entry><title type="html">一次找回GitHub上被删除仓库的经历</title><link href="https://mabbs.github.io/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>https://mabbs.github.io/2025/10/12/recover</id><content type="html" xml:base="https://mabbs.github.io/2025/10/12/recover.html"><![CDATA[<p>在GitHub中寻找踪迹也许是非常简单的事情……<!--more--></p>

<h1 id="起因">起因</h1>
<p>前段时间，有人和我聊天的时候提到了<a href="https://esolangs.org/wiki/Brainfuck">Brainfuck</a>语言，让我回想起了高中时写的<a href="/%E6%BC%94%E8%AE%B2%E7%A8%BF/2018/06/20/Coding.html">演讲稿</a>。那时候我在演讲时也介绍了Brainfuck语言。对于Brainfuck的解释器，<a href="https://rosettacode.org/wiki/RCBF">各种语言都可以实现</a>，不过我当时为了方便理解用了一个在GitHub Pages上的网站，用可视化的方式演示了它的运行过程，效果很不错。现在既然聊到了，自然就想分享一下这个<a href="https://fatiherikli.github.io/brainfuck-visualizer/">演示的网站</a>，但我正想打开时，发现网站已经404了😰。 <br />
  在GitHub Pages上的网站都有对应的仓库，现在不仅原仓库消失了，连作者的<a href="https://github.com/fatiherikli">首页</a>都打不开，看样子是完全退出GitHub了……那么我想找到这个网站的想法就无法实现了吗？不过GitHub有些有意思的特性也许能帮助我找回这个网站。</p>

<h1 id="github的特性">GitHub的特性</h1>
<p>在GitHub中，一个普通的仓库可能没有什么特别的，也许就是服务器上的一个文件夹。但是当仓库被其他人Fork的时候就不一样了，在执行Fork时，显然GitHub不会完整复制整个仓库。否则，同一个仓库在服务器上会占用双倍空间，这显然不合理。另外，想想Git的结构：它由提交对象和分支指针构成，每次提交都有唯一的Hash值且不会冲突。因此可以推测，GitHub在实现Fork时，所有被Fork的仓库可能共享同一个对象库，而每个用户仓库只保存指针，这样所有仓库只会占用增量空间，而不会存储重复内容。 <br />
  但这样也会带来一个问题，首先因为很多人可能要共用一部分对象，所以也很难确认对象的所有权，而且也因为这个原因所有的对象要能被所有人访问。因此在整个Fork网络中，只要有一个仓库存在，GitHub就必须保留所有的对象，而且每个仓库都能访问这个网络中所有的对象。为了验证这一点，我们可以用最知名的<a href="https://github.com/torvalds/linux">Linux内核仓库</a>做个示例。 <br />
  首先对Linux仓库进行Fork，然后我们可以随便做一些改动，比如在README中写“Linux已经被我占领了😆”之类的内容，提交到自己的仓库，并且记下提交的Hash值，接下来就可以把自己的仓库删掉了。如果上面的猜想是正确的，那么在这个Fork网络中的任何一个仓库查看我刚刚的提交应该都可以，于是我直接在主仓库拼上了<a href="https://github.com/torvalds/linux/tree/78e1d0446b94012da8639aa2b157d4f2dee481ce">提交的Hash值</a>（顺便一说只要值唯一，和其他的提交不冲突，<a href="https://github.com/torvalds/linux/tree/78e1d044">短的Hash值</a>也可以），果不其然能找到刚刚修改的内容，这样一来，只要GitHub和任意一个Linux仓库的Fork还存在，这个提交就永远存在了😝。</p>

<h1 id="找回仓库">找回仓库</h1>
<p>那么接下来找回之前网站的方案就很简单了，我只要找到网站仓库的任意一个Fork，然后只要知道最新的提交Hash，我就可以还原最新的仓库了。Fork倒是好找，随便搜一下<a href="https://github.com/ashupk/brainfuck-visualizer">就能找到一个</a>。这个Fork的最新提交是2016年，但要想找到我当年演讲的版本至少到2018年之后。不过这个Hash值也不太好找，虽然理论上爆破短Hash值也可以，但是感觉太麻烦了，没有那个必要，所以我干脆直接去互联网档案馆看看能找到的<a href="https://web.archive.org/web/20201229125043/https://github.com/fatiherikli/brainfuck-visualizer/">最新的仓库页面</a>吧，这样我就能找到它的Hash值了，然后我再把Fork仓库的地址和Hash拼到一起，就看得到最新代码了。 <br />
  当然，仅仅看到代码还不够。我想Fork这个项目并在自己的GitHub Pages上部署一份。有没有什么好办法可以将我仓库的HEAD指针指向最新的提交呢？其实很简单，首先我要Fork这个Fork仓库，然后Clone我的仓库到本地。不过，此时Clone下来的仓库并不包含GitHub上完整的对象库，因此直接checkout或reset是不行的。这时Hash值就派上用场了，通过fetch拉取对应提交后，就可以进行上述操作。具体命令如下：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch origin &lt;commit-hash&gt;
git reset <span class="nt">--hard</span> &lt;commit-hash&gt;
git push origin master
</code></pre></div></div>
<p>最终我就获得了包含<a href="https://github.com/Mabbs/brainfuck-visualizer">最新代码</a>的<a href="https://mabbs.github.io/brainfuck-visualizer/">Brainfuck可视化演示</a>了🎉。</p>

<h1 id="结局">结局</h1>
<p>后来我才知道，原来有一个专门的组织<a href="https://archive.softwareheritage.org">Software Heritage</a>会保存所有代码，根本没必要搞这些花里胡哨的操作😂，像这个仓库也是能很轻易在<a href="https://archive.softwareheritage.org/browse/origin/directory/?origin_url=https://github.com/fatiherikli/brainfuck-visualizer">上面</a>找到，这下以后知道了，再遇到类似情况就可以直接去Software Heritage查找，而不必在互联网档案馆上找线索瞎折腾了🤣。</p>]]></content><author><name>mayx</name></author><category term="GitHub" /><category term="Git" /><category term="代码恢复" /><category term="软件存档" /><summary type="html"><![CDATA[在GitHub中寻找踪迹也许是非常简单的事情……]]></summary></entry><entry><title type="html">关于ZIP Quine与自产生程序的探索</title><link href="https://mabbs.github.io/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>https://mabbs.github.io/2025/09/01/quine</id><content type="html" xml:base="https://mabbs.github.io/2025/09/01/quine.html"><![CDATA[<p>描述自己的代码……是一种什么样的感觉？<!--more--></p>

<h1 id="起因">起因</h1>
<p>前段时间我在折腾<a href="/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">博客部署</a>的时候，回顾起了好久以前写的<a href="/deploy.sh">部署脚本</a>。对于全站打包的这个步骤，本来我打算利用这个压缩包结合<a href="/2025/08/01/sw-proxy.html">Service Worker做离线浏览</a>，但因为没有合适的方案所以放弃了。而现在对于这个压缩包，我又有了一个特别的想法。事实上在这个下载全站的压缩包中，里面的内容和实际的网站并不完全相同，因为在这个压缩包里缺少了压缩包本身。所以把这个压缩包解压之后直接当作网站打开，会发现下载压缩包的链接是无效的，除非在解压之后把压缩包移动到网站里才行…… <br />
  于是我就在想有没有一种可能可以让压缩包解压之后里面又包含了这个压缩包本身？似乎是个不太可能的事情，但我以前听过类似的东西，也许并非不可能？所以这次就来探索一下吧。</p>

<h1 id="自包含压缩包的探索">自包含压缩包的探索</h1>
<p>在很久之前，我见到过一个很知名的自包含压缩包（又称为ZIP Quine），叫做<a href="https://alf.nu/s/droste.zip">droste.zip</a>，是由Erling Ellingsen<a href="https://web.archive.org/web/20090106171423/http://tykje.com/code/useless/zip-file-quine">在2005年制作</a>出来的。当时我只知道它很神奇，原理什么的并不清楚，另外在网上也基本上找不到类似的压缩包。现在再回看时发现<a href="https://alf.nu/ZipQuine">介绍</a>里包含了一些相关的链接，甚至还有一篇能自己制作类似压缩包的论文，所以接下来就可以看一下这些链接来理解这种压缩包是如何制作的了。 <br />
  关于原理方面，先看<a href="https://github.com/wgreenberg">Will Greenberg</a>制作的一个<a href="https://wgreenberg.github.io/quine.zip/">示例</a>，在这里面有一个谜题，使用“print M”（原样输出接下来的M行输入内容）和“repeat M N”（从倒数第N行的输出内容开始，重复M行）这两个指令让最终执行的结果和输入的指令完全相同。这正是对DEFLATE压缩算法所使用的LZ77编码的一种简化模拟，也就是说只要解决了这个问题，就可以让压缩包在解压时原样输出自己了。 <br />
  这个问题看起来还挺复杂，不过在仓库的<a href="https://github.com/wgreenberg/quine.zip/issues/1">Issues</a>就有人给出了几种解法（当然，这个题目解法不唯一），所以在理论上应该是可行的，那么接下来就需要研究压缩文件的格式来实现它了。</p>
<h2 id="实现zip-quine的探索">实现ZIP Quine的探索</h2>
<p>在<a href="https://swtch.com/~rsc/">Russ Cox</a>写的《<a href="https://research.swtch.com/zip">Zip Files All The Way Down</a>》文章中，同样说明了这个原理，而且给出了一个方案，让上述这两个命令除了能够对命令本身的重复以外，还可以添加一些额外数据，这样才能做到构建一个压缩包文件。按照文章的描述，如果用之前谜题的规则来说，我们设头和尾的内容都是“print 0”，那么Cox给出的方案如下：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>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
</code></pre></div></div>
<p>我们把这些指令粘贴到<a href="https://wgreenberg.github.io/quine.zip/">quine.zip</a>这个谜题中，就会发现输出和输入完全相同，以此就能验证Cox方案的正确性。除此之外作者还给出了生成的源代码：<a href="http://swtch.com/rgzip.go">rgzip.go</a>，只是代码里面到处都是用来构建压缩包的十六进制数字，完全看不懂😂。 <br />
  另外这个方案是针对使用基于LZ77与哈夫曼编码的DEFLATE压缩算法，所以格式不重要。因此无论是ZIP，还是GZIP，以及TGZ（GZIP压缩后的TAR），其实都是一样的，因为他们都使用的是DEFLATE压缩算法。顺便一提，<a href="https://github.com/honno">Matthew Barber</a>写了一篇很棒的<a href="https://github.com/honno/gzip-quine">文章</a>，通过动画演示并详细讲解了如何实现一个简单的GZIP版ZIP Quine，很值得一看。 <br />
  还有一点，普通的TAR文件能否实现类似功能呢？从原理来说估计不行，因为TAR文件本身并没有压缩，也不包含指令，就单纯是一堆文件和元数据的拼接，所以就做不到自包含了。 <br />
  这么来看既然TGZ可以，那是不是在我博客网站的压缩包里放一份和自己一模一样的压缩包是可行的？很遗憾按照这个方法来看是做不到的，由于压缩格式和编码的限制，这个方案在实际实现时发现操作码需要是5个字节，最后发现最多只有类似<code class="language-plaintext highlighter-rouge">repeat 64 64</code>这样的指令能够满足要求，因此头尾区最多只能放64-5=59个字节的数据，也就刚刚好能容纳压缩格式需要的内容，几乎没法塞更多东西进去……显然，这些限制导致这种方式对我来说意义就不大了，何况作者的代码我也看不懂……而且还要考虑压缩包还存在校验用的CRC32，需要找满足整个压缩包的CRC32正好在压缩包中的“不动点”。虽然从CRC32的原理来说应该有办法做到通过数学方式解决，但这篇文章的作者因为解决了自包含的问题之后累了，因此放弃继续研究，选择直接暴力破解，毕竟CRC32只有32位，估计思考的时间都要比爆破的时间长吧😂。但如果是这样，即使有方案能存下我博客的数据，也不能在每次网站构建的时候都制作一次了…… <br />
  虽然Russ Cox写的文章看起来做不到包含更多内容了，但Erling Ellingsen制作的droste.zip却包含了一张图片，说明并不是没办法加入更多数据，只是没有找到正确的方法。在2024年<a href="https://github.com/ruvmello">Ruben Van Mello</a>写了一篇论文《<a href="https://www.mdpi.com/2076-3417/14/21/9797">A Generator for Recursive Zip Files</a>》，在这篇论文里他不仅解决了包含的额外数据过少的问题，还编写了一个通用工具，能让普通人也能生成这样的压缩包，而且他还创新性的做了一种像衔尾蛇一样的双层嵌套循环压缩包，非常的有意思，所以接下来我打算试试他的方案。 <br />
  在这篇论文中，里面简述了之前Russ Cox写的内容，也提到了59字节的限制，于是作者对原有的结构进行了一些改动，让操作码可以超出5字节的限制，具体可以看论文的表6，从而解决了只能包含59字节额外数据的限制。但由于DEFLATE压缩格式本身的约束（16位存储块长度以及32KiB回溯窗口），即使能够添加文件，最多也只能额外容纳32763字节的数据（其中包括压缩包所需的文件头）……显然这点空间完全存不下我的博客😭，看来我只能打消这个想法了。但既然都研究了半天，也不一定要存我的博客嘛，可以看看还有没有别的东西可以存？在这之前先继续阅读论文，看完再说吧。</p>
<h2 id="制作一个嵌套循环的zip-quine">制作一个嵌套循环的ZIP Quine</h2>
<p>在实现了常规的ZIP Quine之后，接下来就是作者的创新点了（如果光是解决存储限制这点创新点估计还不够发论文吧😂）。作者接下来制作了一种循环压缩文件，在压缩包内包含文件A和压缩包A，而压缩包A中则包含文件B和最初的压缩包，从而形成一个循环递归的结构。看论文的描述所说如果把外层的压缩包和内层的压缩包的开头和结尾按照一定的规则交替混合，就可以看作是一个整体，然后按照之前做ZIP Quine那样处理就可以……具体实现的细节得看论文的表10。只不过既然是把两个压缩包看作一个整体的话，按照上面的限制，自然每个压缩包能容纳的数据量就更小了，每个最多只能容纳16376字节的数据…… <br />
  另外既然这里面有两个压缩包，那么每个压缩包还有自己的CRC32校验和，理论上如果要爆破的话计算难度得是原来的平方，这样难度就太大了。不过作者发现如果把数据的CRC32值取反（即与“0xFFFFFFFF”取异或）然后和原始数据拼到一起，整个数据的CRC32校验和就会被重置为一个固定的值“0xFFFFFFFF”，看起来挺有意思，正常的哈希算法可没有这种特性。因此原本计算难度很大的爆破计算现在就可以和之前一样了…… <del>话说为什么不让两层的CRC32都这样计算（包括之前单层的ZIP Quine）？这样就不需要爆破了……貌似是因为在普通的ZIP Quine中满足条件的CRC32需要出现两次，所以不能用这个方案吧？</del>  <br />
  现在所有的理论都足够了，我需要挑一个文件来做这样嵌套循环的ZIP Quine，既然博客的大小不可以……要不然我就用我写过的第一个大项目——<a href="https://github.com/Mabbs/Mabbs.Project">Mabbs</a>吧，这个项目的主程序是22KiB，看起来似乎超出了嵌套循环ZIP Quine的限制？其实没有，它的限制指的是压缩后的大小，我这个程序压缩之后是8KiB左右，所以完全没问题。 <br />
  接下来就该使用论文中提到的生成工具：<a href="https://github.com/ruvmello/zip-quine-generator">zip-quine-generator</a>，这是一个Kotlin编写的程序，从发布中可以下载预构建的程序，接下来只要按照README中的描述使用“<code class="language-plaintext highlighter-rouge">--loop</code>”参数就可以用这个程序创建嵌套循环的ZIP Quine了。不过它原本的代码不能修改里面生成的压缩包的名字，另外<a href="https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L845">压缩后的文件属性是隐藏文件</a>，还有<a href="https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L29">生成的压缩包中文件的创建时间总是当前时间</a>，以及<a href="https://github.com/ruvmello/zip-quine-generator/blob/3b8cf977e7a93bb956ad966d5e3b4d503f410529/src/main/kotlin/zip/ZIPArchiver.kt#L30">给文件内填充额外数据的代码里面填的是作者的声明</a>，表示文件是由他论文的所写的生成器生成的……这些情况让我感觉有点不爽，还是希望这些部分能自定义一下，所以我就小改了一下他的代码。顺便一说，Kotlin编译起来还挺简单，直接一句<code class="language-plaintext highlighter-rouge">kotlinc src/main/kotlin -include-runtime -d output.jar</code>就可以了，也不需要折腾Maven之类乱七八糟的东西。最终我修改并编译完程序之后就把文件丢到服务器上开始给我爆破CRC32了，花了10个小时就算出来了，倒是比想象中快😂。 <br />
  （2025.09.26更新）在2025年9月15日的时候，<a href="https://github.com/NateChoe1">Nate Choe</a>给zip-quine-generator做了个<a href="https://github.com/ruvmello/zip-quine-generator/pull/3">重大贡献</a>，他通过<a href="https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm">数学的方式</a>让CRC32的值可以不需要通过爆破的方式算出来，现在想要再制作这样的压缩包就可以瞬间生成了……要是我再晚点做这个压缩包就不需要花那么长时间了吧🤣。 <br />
  最终我给我的<a href="https://github.com/Mabbs/Mabbs.Project">Mabbs</a>项目创建了<a href="https://github.com/Mabbs/Mabbs.Project/releases/tag/Final-version">Infinite Mabbs</a>这个发布，生成的文件也可以在<a href="/assets/Mabbs.zip">这里</a>下载，这也算是不枉我研究半天这个论文了😆。</p>

<h1 id="自产生程序的探索">自产生程序的探索</h1>
<p>说起来自包含压缩包为什么叫做ZIP Quine？其中的Quine是什么意思呢？其实这是一位美国哲学家的名字，他提出了“自指”的理论概念，所以为了纪念他，有类似概念的东西就被称作Quine，具体为什么也可以去看<a href="https://en.wikipedia.org/wiki/Quine_(computing)#Name">维基百科</a>的说明。现在提到Quine一般代表的就是自产生程序，而自包含压缩包因为实现的原理和自产生程序的原理差不多，所以叫做ZIP Quine。因此接下来我打算探索一下自产生程序，更深入地了解Quine。</p>
<h2 id="实现quine的探索">实现Quine的探索</h2>
<p>那么什么是自产生程序？简单来说就是程序的源代码和程序的输出完全相同的程序，而且通常来说不允许通过读取/输入源代码的方式实现。按照一般的想法，让程序输出自身就需要输出中有全部代码，整个代码就会变长，而更长的代码就要输出更多，然后代码就会越来越长……所以这么想来似乎成了个死胡同。但其实这种程序实现起来并不复杂，想想ZIP Quine的实现，关键在于指令还需要以数据的形式表现，并且能被引用，这样输出的时候就会连着指令一起输出了。比如用Python的Quine举例：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">c</span> <span class="o">=</span> <span class="s">'c = %r; print(c %% c)'</span><span class="p">;</span> <span class="k">print</span><span class="p">(</span><span class="n">c</span> <span class="o">%</span> <span class="n">c</span><span class="p">)</span>
</code></pre></div></div>
<p>这里的变量中就以数据的形式存储了程序的代码，而在输出的时候除了变量内的代码，又通过引用的方式又把变量的内容放回到赋值的地方，所以它的输出就和原本的代码一样了。 <br />
  其实Quine的实现思路都差不多是这样，可以在<a href="https://rosettacode.org/">Rosetta Code</a>中找到<a href="https://rosettacode.org/wiki/Quine">各种语言实现的Quine</a>，在这其中能够发现大多数高级语言的写法都是类似的，除了一些低级语言以及esolang……这些我也看不懂😂，主要是有些语言没有变量的概念，不知道是怎么区分代码和数据……除了那个网站，在<a href="https://esolangs.org/wiki/List_of_quines">这里</a>还能找到更多由esolang编写的Quine，可以看出来基本上很难看懂，其中最令人望而生畏的还得是<a href="https://lutter.cc/malbolge/quine.html">用Malbolge写的Quine</a>，这个代码看起来不仅很长，而且像乱码一样。至于什么是Malbolge？这就是Malbolge程序：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>D'&lt;;_98=6Z43Wxx/.R?Pa
</code></pre></div></div>
<p>代码就像加了密似的，顺便一说这个执行的输出结果是“Mayx”，关于Malbolge的具体细节可以看它的<a href="http://www.lscheffer.com/malbolge_spec.html">规范</a>，另外虽然这个语言写起来很复杂，但还是有人能用它编出程序的，甚至还有人用<a href="https://esolangs.org/wiki/Malbolge_Unshackled">Malbolge Unshackled</a>（Malbolge不限内存的变种）写过<a href="https://github.com/iczelia/malbolge-lisp">Lisp解释器</a>，实在是恐怖如斯😨。</p>
<h2 id="只能quine的语言">只能Quine的语言</h2>
<p>其实想要做出Quine，还有一种更加无聊的方案，那就是设计一种只能Quine的语言🤣。根据Quine的定义，代码输出的结果就是它本身……所以我们可以把任何内容都看作代码，然后这种语言的行为就是输出所有代码……听起来是不是有点无聊？但是想想看如果把Linux中的cat命令当作解释器，就可以实现这种语言了，比如：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/cat
Hello, world!
</code></pre></div></div>
<p>作为脚本执行的结果就是原样输出这段内容，不过把内容当作代码算不算作弊呢……如果看作是cat的输入显然是作弊，但如果是当作源代码的话应该就不算了吧😋……但这就不是能写出逻辑的语言了。所以说Quine的趣味并不在“能不能实现”，而在于如何在限制条件下实现。正是因为大多数语言不会直接“自我输出”，才会觉得那些精巧的Quine程序如此有意思。</p>
<h2 id="quine-relay的探索">Quine Relay的探索</h2>
<p>还有一个更加复杂的Quine变种是“Quine接力”（Quine Relay），即一个程序输出另一个程序的源代码，另一个程序又输出下一个程序的源代码，最后回到原始程序，就和之前所说的嵌套循环ZIP Quine有点类似。最著名的例子是<a href="https://github.com/mame">Yusuke Endoh</a>（这位还是<a href="https://www.ioccc.org/">IOCCC</a>的冠军之一）创建的<a href="https://github.com/mame/quine-relay">quine-relay</a>项目，它包含了128种编程语言的循环。 <br />
  这种程序写起来会更复杂一些，不过原理都差不多，通常除了当前运行的部分是可执行代码外，其他的代码都需要以额外包含的数据形式（如字符串）存储在变量中。如果想自己做个类似简单的Quine Relay，除了去看<a href="https://en.wikipedia.org/wiki/Quine_(computing)#Ouroboros_programs">维基百科</a>之外，前段时间我还看到过一个不错的<a href="https://blog.mistivia.com/posts/2024-09-21-quine/">文章</a>，里面就讲了如何用“笨办法”编写Quine和Quine Relay，通过把变量中的内容编码为16进制来避免不同语言可能存在的特殊字符转译问题，思路不错，对于理解如何编写这类程序的问题很有帮助。当然这只是个<strong>简单</strong>的方案，仅适用于一些常规的编程语言，像上面那个<a href="https://github.com/mame/quine-relay">quine-relay</a>项目中甚至还包含Brainfuck之类的esolang，这种估计得要想办法让相对高级一些的语言通过“生成”的方式得到输出下一种代码的代码，而不是简单的赋值了，所以只靠这点知识想去完全理解大佬的作品还是想多了😆。 <br />
  顺便一说，quine-relay并不是那位大佬唯一的Quine作品，他还做过<a href="https://github.com/mame/radiation-hardened-quine">有冗余的Quine</a>以及<a href="https://mamememo.blogspot.com/2010/09/qlobe.html">动态的Quine</a>，真的是相当的厉害……</p>
<h2 id="polyglot-quine的探索">Polyglot Quine的探索</h2>
<p>除了Quine Relay之外还有一种很复杂的Quine，叫做<a href="https://en.wikipedia.org/wiki/Polyglot_(computing)">Polyglot</a> Quine，与Quine Relay需要在程序执行后才能切换到其他语言接力不同，Polyglot Quine的源代码本身即可同时属于多种语言，而且用这些语言的解释器每个执行后的输出全都一样，都与源代码完全一致。由于不同的编程语言的格式既有些相同之处，也有很多不同之处，所以让同一份代码表示不同语言就会很容易产生歧义，这时候就只能想办法通过一些特别的方式（比如将可能会对当前语言产生干扰的代码看作是注释的方式）来规避语言之间的差异。 <br />
  Quine本身就已经很困难了，再加上这些限制就变得更加复杂了，所以制作Polyglot Quine的编程语言基本上都得精挑细选，而且通常只有两种语言，比如<a href="https://github.com/TrAyZeN/polyglot-quine/blob/master/main.c">这段代码</a>就是C和Python的Polyglot Quine，它巧妙利用了C预处理器指令在Python中可视为注释的特性，使两种语言互不干扰，非常有趣。当然并不是说只能是两种语言，像<a href="https://github.com/2KAbhishek/polyquine">这个</a>项目甚至使用了五种语言（C、Perl、PHP、Python、Ruby），可以说是相当厉害了。除此之外更令人惊叹的则是<a href="https://github.com/d0sboots/PyZipQuine">PyZipQuine</a>项目，在这其中LZ77编码也可以作为一种语言，所以既可以被当作压缩包，也可以作为Python2.7代码，而且二者都是Quine，实在是令人赞叹。</p>

<h1 id="感想">感想</h1>
<p>虽然这次探索最终没能完成让包含博客所有内容的压缩包自包含，但是在探索的过程中我还是收获了不少，尤其是Ruben Van Mello制作的ZIP Quine生成工具，实在是太棒了。很久以前我见到droste.zip这个压缩包的时候，就想整一个属于自己的ZIP Quine，现在我不仅用那个生成工具做了一个，还是对我来说很有意义的第一个项目——Mabbs，而且更关键的还是生成的是比普通的ZIP Quine更高级的嵌套循环ZIP Quine，也算是圆了小时候的心愿了。 <br />
  另外在探索自产生程序的时候，也发现了一些很有意思的网站，比如<a href="https://rosettacode.org/">Rosetta Code</a>以及<a href="https://esolangs.org/">Esolang wiki</a> <del>（虽然这个网站里被好多小学生写了一堆无聊的东西😂）</del> ，里面有不少有趣的东西，也算是让我大开眼界了。 <br />
  所以有的时候探索不一定要完成目标，在这个过程中也会收获到很多不错的东西吧😊。</p>]]></content><author><name>mayx</name></author><category term="压缩包" /><category term="Quine" /><category term="自产生程序" /><category term="Quine Relay" /><summary type="html"><![CDATA[描述自己的代码……是一种什么样的感觉？]]></summary></entry></feed>