<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/atom-styles.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>RefactX</title>
  <subtitle>你与光芒 / You &amp; Radiance。这里记录构建网站的过程、日常的片段，以及那些值得被留住的光。</subtitle>
  <link href="https://www.refact.cc//atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.refact.cc/" rel="alternate" type="text/html"/>
  <updated>2026-11-12T12:32:58.421Z</updated>
  <language>zh-CN</language>
  <id>https://www.refact.cc//</id>
  <author>
    <name>Refac7</name>
    <uri>https://www.refact.cc/</uri>
  </author>
  <generator uri="https://github.com/Dnzzk2/Litos" version="5.0">Astro Litos Theme</generator>
  <rights>Copyright © 2026 Refac7</rights>
  
  <entry>
    <title>Nothing Phone (4a) 解锁 Bootloader 与 KernelSU Root 完整教程</title>
    <link href="https://www.refact.cc//posts/post-10" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-10</id>
    <updated>2026-11-04T00:00:00.000Z</updated>
    <published>2026-11-04T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">记录 Nothing Phone (4a) 解锁 Bootloader、修补并刷入 init_boot 镜像，以及通过 KernelSU 获取 Root 权限的完整过程，同时介绍 Nothing OS 5.0 手动刷机与重新获取 Root 的方法。</summary>
    <content type="html"><![CDATA[
<p>本文主要介绍了给 Nothing Phone (4a) 解锁 Bootloader 以及获取 Root 权限的全过程，包括系统初始设置、KernelSU 安装、Nothing OS 5.0 手动刷机及重新获取 Root 权限。</p>
<div><div><div></div><div>WARNING</div></div><div><p><strong>刷机与 Root 操作具有一定风险，请务必在开始前仔细阅读。</strong></p><ol>
<li><strong>数据丢失：</strong> 解锁 Bootloader 会清除手机上的所有用户数据，包括照片、文件、应用及账号信息。请提前完成完整备份。</li>
<li><strong>设备变砖：</strong> 刷入错误的镜像、使用不匹配的固件版本或在刷写过程中断开连接，都可能导致系统无法正常启动，严重时甚至可能造成设备变砖。</li>
<li><strong>系统更新：</strong> 获取 Root 权限后，OTA 更新可能出现安装失败、校验异常等问题。系统升级后，原有 Root 权限也可能失效，需要重新修补并刷入对应镜像。</li>
<li><strong>保修与安全性：</strong> 解锁 Bootloader 和获取 Root 权限可能影响设备的保修资格、系统完整性校验及部分依赖安全认证的应用正常使用。具体以购买地区的保修政策及实际故障原因判定为准。</li>
<li><strong>文件版本匹配：</strong> 请务必确认下载的固件、<code>boot.img</code> 或 <code>init_boot.img</code> 与当前设备型号及系统版本完全对应，不要直接使用其他版本的镜像。尤其注意区分 Nothing Phone (4a) 和 Nothing Phone (4a) Pro。</li>
<li><strong>操作环境：</strong> 刷写过程中请确保手机电量充足、USB 连接稳定，并正确安装 ADB / Fastboot 驱动及工具。</li>
<li><strong>原厂镜像备份：</strong> 在获取 Root 权限前，请妥善保存对应系统版本的原厂 <code>init_boot.img</code>，以便出现启动异常时恢复。</li>
</ol><p><strong>请注意：</strong> 本文仅记录笔者在 Nothing Phone (4a) 上的实际操作过程，不保证适用于所有设备、固件版本及使用环境。所有操作均由读者自行决定并承担相应风险。</p></div></div>
<h2>一、了解相关概念</h2>
<p>首先我们要了解几个概念：</p>
<ul>
<li>
<p><strong>fastboot</strong>：</p>
<p>一种底层刷机协议。手机进入该模式后，可通过电脑命令行直接向手机底层分区刷写文件，是解锁和刷入 Root 的核心工具。</p>
</li>
<li>
<p><strong>bootloader 锁</strong>：</p>
<p>手机系统的第一道安全防线。默认处于锁定状态，解锁后才能刷入第三方系统或获取 Root，但代价是<strong>清空所有数据（请注意在开始前备份重要数据）</strong>。解锁 Bootloader 和获取 Root 权限也可能影响设备的保修资格及部分安全认证功能。</p>
</li>
<li>
<p><strong>KernelSU</strong>：</p>
<p>一种基于内核层的 Android Root 解决方案。它通过内核机制管理应用的 Root 权限，并支持安装各类功能模块。</p>
</li>
</ul>
<h2>二、解锁 Bootloader</h2>
<p>对于 Nothing Phone 来说，我们只需要进入 fastboot 即可使用电脑的命令行解锁 bootloader，无需进行额外的操作。</p>
<p>在开始之前，我们建议退出 Google 账号，以降低恢复出厂设置后触发账号验证、无法完成 OOBE 初始设置的风险。</p>
<h3>1. 开启开发者选项</h3>
<p>进入设置，进入“关于本机”，进入左上角 NOTHING OS X.X 卡片的“软件信息”页面，连续点击七次版本号，输入本机密码即可进入开发者模式。</p>
<p>回到设置主页面，进入“系统”，进入“开发者选项”，打开“OEM 解锁”和“USB 调试”，可能需要根据提示输入密码。</p>
<h3>2. 安装 ADB 和 Fastboot 工具</h3>
<p>来到电脑，下载 <a href="https://developer.android.com/tools/releases/platform-tools?hl=zh-cn" rel="noopener noreferrer" target="_blank">Android SDK Platform Tools</a>，根据提示下载对应系统的版本，并完成解压。</p>
<p>我使用的是 Arch Linux，在终端输入：</p>
<pre><code>sudo pacman -S android-tools
</code></pre>
<h3>3. 连接手机并验证 ADB</h3>
<p>打开 CMD 或者终端，导航到解压完成的文件夹，用数据线将手机连接至电脑，在手机上确认允许 USB 调试，使用：</p>
<pre><code>adb devices
</code></pre>
<p>查看连接到的设备，出现一串识别码，后面跟上 <code>device</code> 就算成功了。</p>
<h3>4. 进入 Fastboot 并解锁</h3>
<p>输入：</p>
<pre><code>adb reboot bootloader
</code></pre>
<p>此时手机会重启并进入 fastboot 模式，再次验证连接。</p>
<p>输入：</p>
<pre><code>fastboot devices
</code></pre>
<p>并回车。我们如果看到同样的识别码，后面跟上 <code>fastboot</code> 就算连接成功。</p>
<p>输入：</p>
<pre><code>fastboot flashing unlock
</code></pre>
<p>解锁 bootloader，用音量键选择上下，电源键确定，确认打开 bootloader 即可。</p>
<p><strong>注意：</strong> 确认解锁后，手机会清除所有用户数据。请耐心等待设备完成操作，不要在此过程中断开连接。</p>
<h2>三、完成初始系统设置</h2>
<p>手机重启，看到黄色提示，和一串英文是正常的，大概就是提示你 bootloader 已解锁，按下电源键停止启动之类的话，不用管。</p>
<p>进入系统，完成 OOBE 设置。</p>
<p>如果说你之前没有退出谷歌账号的话，系统可能会要求你验证此前绑定的 Google 账号。这属于 Android 的恢复出厂设置保护机制（FRP）。</p>
<p>可是国内没有直接连接谷歌的条件，Wi-Fi 也无法设置代理，如果你卡在这一步了，可以尝试使用电脑开启代理热点。</p>
<p>此处提供 Linux 版本，如为其他系统，请提供给 AI 转换。</p>
<img src="https://img.refact.cc/1793758505163-3jkdlje3pq.jpg" alt="代理热点设置" />
<h3>1. 下载脚本</h3>
<pre><code>sudo curl -o /usr/local/bin/lnxrouter https://raw.githubusercontent.com/garywill/linux-router/master/lnxrouter
</code></pre>
<h3>2. 赋予权限</h3>
<pre><code>sudo chmod +x /usr/local/bin/lnxrouter
</code></pre>
<h3>3. 安装依赖</h3>
<pre><code>sudo pacman -S bash procps-ng iproute2 dnsmasq iptables hostapd iw wireless-tools haveged
</code></pre>
<h3>4. 确认无线网卡名称</h3>
<p>不同 Linux 设备的无线网卡名称可能不同，不一定都是 <code>wlan0</code>。</p>
<p>使用以下命令查看网卡：</p>
<pre><code>ip link
</code></pre>
<p>确认无线网卡名称后，以下操作中的 <code>wlan0</code> 均替换为实际名称。</p>
<h3>5. 解除网卡占用</h3>
<pre><code>sudo nmcli device set wlan0 managed no
</code></pre>
<h3>6. 开启热点</h3>
<pre><code>sudo lnxrouter --ap wlan0 MyAccessPoint -p MyPassPhrase -c 6 --freq-band 2.4
</code></pre>
<h3>7. 配置代理并连接热点</h3>
<p>打开你的代理软件，以 FlClash 为例，打开局域网代理和系统代理。</p>
<p><strong>这里需要特别注意：开启局域网代理和系统代理，并不意味着连接电脑热点的手机会自动使用代理。</strong></p>
<p>系统代理通常只影响运行代理软件的电脑，而局域网代理只是向其他设备开放代理服务。</p>
<p>如果需要让手机通过电脑的代理访问 Google 服务，还需要满足以下条件之一：</p>
<ul>
<li>在手机端能够设置代理的情况下，手动填写电脑的局域网 IP 地址和 FlClash 提供的代理端口。</li>
<li>如果 OOBE 阶段无法手动设置代理，则需要额外配置电脑的透明代理或网关流量转发，让连接热点的设备流量经过代理。</li>
</ul>
<p>因此，仅仅开启 FlClash 的局域网代理和系统代理，不能保证手机在 OOBE 阶段正常连接 Google 服务。请根据实际网络环境完成代理配置，并先验证手机能够访问所需服务。</p>
<p>随后连接到此热点完成后续设置即可。</p>
<p>完成操作后，如果不再需要热点，可以恢复 NetworkManager 对无线网卡的管理：</p>
<pre><code>sudo nmcli device set wlan0 managed yes
</code></pre>
<h2>四、获取 Root 权限</h2>
<p>按照之前的方法，重新打开开发者选项，打开“USB 调试”。</p>
<h3>1. 下载 KernelSU 和 Boot 镜像</h3>
<p>进入桌面后，安装 <strong><a href="https://github.com/tiann/KernelSU" rel="noopener noreferrer" target="_blank">KernelSU</a></strong>。</p>
<p>从 <a href="https://nothingarchive.tech/docs/firmware#frogger" rel="noopener noreferrer" target="_blank">Nothing Archive 固件页面</a> 下载对应自己系统的 boot 镜像。</p>
<p>以 <a href="https://nothingarchive.tech/docs/changelogs/frogger/Frogger-B4.1-260911-0959" rel="noopener noreferrer" target="_blank">Frogger-B4.1-260911-0959</a> 为例，下载：</p>
<p><a href="https://github.com/spike0en/nothing_archive/releases/download/Frogger_B4.1-260911-0959/Frogger_B4.1-260911-0959-image-boot.7z" rel="noopener noreferrer" target="_blank">下载 Frogger-B4.1-260911-0959-image-boot.7z</a></p>
<p>解压，观察到 <code>init_boot.img</code> 文件，大小是 8MB。</p>
<p><strong>注意：</strong> 文件大小仅供参考，不能作为镜像正确性的判断依据。请务必确认固件对应 Nothing Phone (4a)，并且与手机当前运行的系统版本一致。</p>
<h3>2. 检查 KernelSU 支持状态</h3>
<p>打开 KernelSU，检查设备的安装状态。</p>
<p>如果显示 <code>Not installed</code>，并且设备支持相应安装方式，可以继续进行修补。</p>
<p>如果显示 <code>Unsupported</code>，不要直接按照本文的方法刷入镜像，应先确认设备的内核及 KernelSU 兼容性。</p>
<h3>3. 备份原厂 init_boot.img</h3>
<p>在修补之前，请将解压得到的原厂 <code>init_boot.img</code> 单独复制一份，保存在电脑上。</p>
<p>建议按照系统版本命名，例如：</p>
<pre><code>Frogger_B4.1/
├── init_boot.img
└── init_boot_original.img
</code></pre>
<p>这样即使修补后的镜像无法正常启动，也可以通过 Fastboot 恢复原厂镜像。</p>
<h3>4. 修补 Boot 镜像</h3>
<p>打开 KernelSU，进入最上面的未安装，选择用 boot 镜像修补，选择 <code>init_boot.img</code> 进行修补。</p>
<p>修补后的文件会保存在原路径下，将修补后的镜像传入电脑。</p>
<p>注意不要覆盖之前保存的原厂镜像。</p>
<h3>5. 刷入修补后的镜像</h3>
<p>按照之前的方法将手机连接电脑，进入 ADB。</p>
<p>输入：</p>
<pre><code>adb reboot bootloader
</code></pre>
<p>此时手机会重启并进入 fastboot 模式，再次验证连接：</p>
<pre><code>fastboot devices
</code></pre>
<p>确认设备已经连接后，检查当前活动槽位：</p>
<pre><code>fastboot getvar current-slot
</code></pre>
<p>记下返回的活动槽位信息，确保接下来使用的修补镜像与当前系统版本及活动槽位对应。</p>
<p>然后执行：</p>
<pre><code>fastboot flash init_boot "/path/to/patched_init_boot.img"
</code></pre>
<p>将 <code>/path/to/patched_init_boot.img</code> 替换为电脑上实际的修补镜像路径。</p>
<img src="https://img.refact.cc/1793758570597-xjhr3qfl3hn.jpg" alt="刷入修补后的镜像" />
<p>最后输入：</p>
<pre><code>fastboot reboot
</code></pre>
<p>重启手机，进入 KernelSU，如果看到已激活，则 Root 成功。</p>
<img src="https://img.refact.cc/1793758585850-hlm9bw7xbd7.jpg" alt="KernelSU 激活状态" />
<h3>6. 恢复原厂 init_boot.img</h3>
<p>如果刷入修补镜像后无法正常启动，或者需要恢复原厂启动环境，可以重新进入 Fastboot，使用此前保存的原厂镜像进行恢复。</p>
<pre><code>adb reboot bootloader
</code></pre>
<p>确认设备连接：</p>
<pre><code>fastboot devices
</code></pre>
<p>恢复原厂镜像：</p>
<pre><code>fastboot flash init_boot "/path/to/init_boot_original.img"
</code></pre>
<p>重启设备：</p>
<pre><code>fastboot reboot
</code></pre>
<p><strong>注意：</strong> 恢复时必须使用与当前系统版本及活动槽位对应的原厂镜像。如果系统已经升级，不能继续使用旧版本的镜像进行恢复。</p>
<p>Root 完成后，建议妥善保存对应版本的原厂 <code>boot.img</code>、<code>init_boot.img</code> 以及完整固件包。</p>
<p>如果需要进行完整分区备份，应使用适合设备分区布局的备份工具，并确认备份内容可以正常恢复。</p>
<img src="https://img.refact.cc/1793758995042-2ch1c83oi4.jpg" alt="全字库备份" />
<h2>五、升级 Nothing OS 5.0 并重新获取 Root</h2>
<p>在笔者 root 完成的第二天，nothing 推送了 nothing OS 5.0 更新，发现无法进行 OTA 更新，在更新页面提示安装出现问题，尝试多次仍未成功，于是笔者决定直接刷入 OS 5.0。</p>
<p>Root 修改了系统启动相关镜像，可能影响 OTA 更新过程中的分区校验和安装流程。不同系统版本及安装方式的具体情况有所不同。</p>
<img src="https://img.refact.cc/1793758610676-6u6nde7fuic.jpg" alt="OTA 更新失败" />
<h3>1. 下载官方固件归档</h3>
<p>首先在电脑下载 Nothing OS 官方全量固件的第三方归档：</p>
<p><a href="https://github.com/spike0en/nothing_archive/releases/tag/Frogger_C5.0-261023-2145" rel="noopener noreferrer" target="_blank">Nothing Archive - Frogger C5.0-261023-2145</a></p>
<p>（7z 分片，需在电脑解压）。</p>
<p><strong>注意：</strong> 此处使用的是 Nothing OS 官方固件的第三方归档，并非 Nothing 官方下载站。</p>
<p>请确认下载的是 Frogger 对应的 Nothing Phone (4a) 固件，不要误用 FroggerPro 的固件。</p>
<h3>2. 下载 Phone (4a) 专用线刷脚本</h3>
<p>然后使用网上找到的线刷脚本：</p>
<p><a href="https://github.com/spike0en/nothing_flasher" rel="noopener noreferrer" target="_blank">spike0en/nothing_flasher</a></p>
<p>该项目为不同 Nothing 手机提供了不同的刷机脚本。</p>
<p>Nothing Phone (4a) 应使用 Frogger 专用分支：</p>
<ul>
<li><a href="https://github.com/spike0en/nothing_flasher/blob/frogger/Windows/flash_all.bat" rel="noopener noreferrer" target="_blank">Windows 版本</a></li>
<li><a href="https://github.com/spike0en/nothing_flasher/blob/frogger/bash/flash_all.sh" rel="noopener noreferrer" target="_blank">Bash / Linux 版本</a></li>
</ul>
<p>不要使用其他机型的脚本。</p>
<h3>3. 执行全量刷写</h3>
<p>将完整解压后的固件镜像文件与对应系统的刷机脚本放在一起。</p>
<p>Linux 用户可以在终端进入对应目录，执行：</p>
<pre><code>chmod +x flash_all.sh
bash flash_all.sh
</code></pre>
<p>Windows 用户则运行对应的 <code>flash_all.bat</code>。</p>
<p>确保手机已经进入 Bootloader / Fastboot 模式，并且电脑能够正常识别设备。</p>
<p>脚本会检查当前活动槽位，并根据设备情况进入 Fastbootd，随后执行分区刷写。</p>
<p><strong>关于数据清除：</strong></p>
<p>脚本执行过程中会询问：</p>
<pre><code>Wipe Data? (Y/N)
</code></pre>
<p>选择 <code>Y</code> 会清除 <code>userdata</code> 和 <code>metadata</code> 分区中的数据。</p>
<p>选择 <code>N</code> 则跳过这一步，但不代表一定能够保留原有用户数据。跨版本升级、分区布局变化或系统兼容性问题仍可能导致无法正常启动。</p>
<p>因此，无论选择哪一项，都建议提前备份重要数据。</p>
<p><strong>关于 A/B 槽位：</strong></p>
<p>Nothing Flasher 的 Frogger 专用脚本会将相关镜像刷入非活动槽位，并在完成刷写后切换活动槽位。</p>
<p>不要在刷写过程中断开 USB 连接，也不要在所有分区刷写完成之前手动重启设备。</p>
<p>如果脚本提示某个分区刷写失败，不要在不了解原因的情况下选择继续执行。</p>
<img src="https://img.refact.cc/1793758649050-b2ibjy08iq.png" alt="Nothing OS 5.0" />
<h3>4. 重新获取 Root 权限</h3>
<p>随后成功刷入 OS 5.0。</p>
<p>由于全量刷机过程会重新写入原厂启动镜像，之前通过 KernelSU 修补的 <code>init_boot.img</code> 也会被覆盖，因此原有 Root 权限会失效。</p>
<p>进入系统后，重新开启开发者选项及 USB 调试。</p>
<p>从对应的 C5.0 固件中提取原厂 <code>init_boot.img</code>，使用 KernelSU 重新修补。</p>
<p>之后按照第四章的方法，将修补后的镜像刷入当前活动槽位对应的 <code>init_boot</code> 分区。</p>
<p>重启手机，进入 KernelSU，如果看到已激活，则重新获取 Root 权限成功。</p>
<h2>六、特别鸣谢</h2>
<p>感谢 <a href="https://rainview.net/" rel="noopener noreferrer" target="_blank">RainView</a> 在技术上提供的支持和帮助。</p>]]></content>
    <category term="Android" />
    <category term="Root" />
    <category term="刷机" />
    <category term="教程" />
    <category term="玩机" />
    <category term="系统" />
    <category term="折腾" />
    <category term="备份" />
  </entry>
  <entry>
    <title>我到底是怎么了？</title>
    <link href="https://www.refact.cc//posts/post-09" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-09</id>
    <updated>2026-10-05T00:00:00.000Z</updated>
    <published>2026-10-05T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">休学五个月，我仍在学着与自己的身体共处；想改变，却发现一切都还那么遥远。</summary>
    <content type="html"><![CDATA[
<p>转眼又到了国庆节。休学，已经五个月了。</p>
<p>感觉一切都是徒劳呢。我每天晚上将不精确的2g自制的雌二醇凝胶涂在自己大腿内侧，希望这样能带我迈入女孩子的行列。可是呢？乳房发育了，长在了一个彻头彻尾的男人身上，在别人看来，和怪物又有什么区别呢？</p>
<p>我这肮脏的身体呵，每天早上照镜子时，我心里总有将其毁掉的冲动，我就会考虑自残。我每次自残后必须将受伤的部位躲着家长，对外必须表现得像个正常人，才能避免再次被关在医院里。你是一个男生，你不能示弱，你要坚强。可明明，我才想成为被爱的那个人啊。</p>
<img src="https://img.refact.cc/1791204057729-rog9pc7t7s.jpg" alt="" />
<p>我只能把窗帘死死拉上，缩在昏暗的房间里，整天躲在里面，不敢见人。走在街上，总觉得路人都在看我，仿佛他们一眼就能看穿我宽大外套下垫着的小背心，看穿我一直想藏起来的那些东西。</p>
<p>我也想拥有正常的青春啊，想在秋天喝好朋友请的第一杯奶茶，想和好朋友手挽手去漫展，想被好好爱惜一次。可我连用标准的女性嗓音说一句“谢谢”都不敢。每次开口，喉咙里发出的低沉声音都在嘲笑我的不自量力。我拼命咽下那些补佳乐和色普龙，却还是忍不住去涂那些毫无保证的凝胶，好像多涂一点点，这副身体就能快一点变化，就能快一点长出柔软的曲线。但那只是徒劳，对吧？生物学是那么残忍，好像从出生开始，很多东西就已经定下来了，让我怎么都摆脱不了。</p>
<p>有时候，拿着刀片抵在手腕上的时候，我甚至会觉得有一种解脱的平静。鲜血流出来的那一刻，痛觉起码是真实且完全属于我自己的。但我并不是喜欢这样做。我怕疼，更怕被父母发现后，被当作精神失常的变态强行送进那种可怕的医院。我只能小心翼翼地用长袖遮住那些新旧交叠的疤痕，在洗澡时对着镜子里的那具躯壳无声地干呕。</p>
<img src="https://img.refact.cc/1791204072989-6klhxnjvppn.jpg" alt="" />
<p>我好累。</p>]]></content>
    <category term="情绪" />
    <category term="随笔" />
  </entry>
  <entry>
    <title>如何自制雌二醇凝胶</title>
    <link href="https://www.refact.cc//posts/post-08" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-08</id>
    <updated>2026-09-09T00:00:00.000Z</updated>
    <published>2026-09-09T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">从器材、原料到配制步骤，记录一份 0.1% 雌二醇凝胶的自制方法及风险提示。</summary>
    <content type="html"><![CDATA[
<blockquote><p>本文档非专业医疗文档，此文档不完整、不精确、且不具备可重复性，仅供参考。此行为可能会对机体造成重大影响，本文不对任何人的任何不良反应负责，若你不同意以上内容，请立即退出本文。</p></blockquote>
<img src="https://img.refact.cc/1790500886373-zq7vsct13oo.jpg" alt="" />
<h2>本配方可能用到的器械（无序列表）：</h2>
<ul>
<li>毫克称；</li>
<li>克称或量筒；</li>
<li>烧杯若干；</li>
<li>胶头滴管；</li>
<li>药匙；</li>
<li>玻璃棒或磁力搅拌器；</li>
<li>pH试纸；</li>
</ul>
<h2>本配方可能用到的材料及其价格（单位均为人民币，按必要性排序）：</h2>





































<table><thead><tr><th>项目名</th><th>价格</th></tr></thead><tbody><tr><td>β-雌二醇</td><td>30元/g</td></tr><tr><td>95%乙醇(v/V)</td><td>5元/500mL</td></tr><tr><td>卡波姆980</td><td>13元/50g</td></tr><tr><td>三乙醇胺</td><td>5元/50g</td></tr><tr><td>氮酮（可选）</td><td>8元/500g</td></tr><tr><td>丙二醇（可选）</td><td>6元/100mL</td></tr><tr><td>纯化水</td><td>2元/500mL</td></tr></tbody></table>
<h2>以下为100g质量分数为0.10%凝胶的制作步骤：</h2>
<ol>
<li>用克称量取<strong>63.0g</strong>乙醇溶液（95%(v/V)），倒入烧杯；</li>
<li>用毫克称量取<strong>0.100g</strong>雌二醇粉末，加入烧杯，搅拌使其完全溶解，此时溶液无色透明状态；</li>
<li>用克称量取<strong>28.5g</strong>纯化水，加入烧杯，搅拌均匀；</li>
<li>用毫克称量取<strong>1.000g</strong>卡波姆980粉末，少量多次加入烧杯，避免出现结块，搅拌一段时间后，停顿，再搅拌，重复若干次，使其溶解，如果非磁力搅拌器，可能无法完全溶解，此时溶液应半透明偏白；</li>
<li>（可选）用毫克称量取<strong>5.000g</strong>丙二醇，加入烧杯，搅拌均匀；</li>
<li>（可选）用毫克称量取<strong>1.000g</strong>氮酮，加入烧杯，搅拌均匀；</li>
<li>用毫克称量取<strong>1.350g</strong>三乙醇胺溶液，加入烧杯，搅拌均匀，搅拌过程中，烧杯内溶液逐渐变黏稠，至此，我们制得约<strong>100g</strong>雌二醇凝胶(0.1%)，市售凝胶为(0.06%)，必须根据定期检验报告，自身吸收效率，促渗剂比例等调整质量分数。</li>
</ol>
<p>用pH试纸测试pH，应在6.5附近。卡波姆980的最佳中和pH值为6.0-6.5。pH若低于5.5，凝胶会变稀且严重刺激皮肤导致红肿溃烂；pH若高于7.5，雌二醇在碱性环境下极易降解失效。本配方无防腐剂，凝胶可能发霉导致皮肤感染，需要了解该风险。</p>
<p>最后提示：<strong>自行调配含雌二醇的外用制剂，属于自制药品</strong>。根据《中华人民共和国药品管理法》，未经许可配制药品属于违法行为；如果对外出售，会按生产假药 / 劣药处罚。以及…请务必珍视自身健康。</p>
<p>参考文章：</p>
<ul>
<li><a href="https://github.com/lydlid/estrogel-diy-guide-zh_CN" rel="noopener noreferrer" target="_blank">https://github.com/lydlid/estrogel-diy-guide-zh_CN</a></li>
<li><a href="https://github.com/Bluse-liu50/DIY-Estradiol-Gel-Guide" rel="noopener noreferrer" target="_blank">https://github.com/Bluse-liu50/DIY-Estradiol-Gel-Guide</a></li>
</ul>]]></content>
    <category term="雌二醇" />
    <category term="自制" />
    <category term="药理" />
  </entry>
  <entry>
    <title>HRT一个月以来的变化</title>
    <link href="https://www.refact.cc//posts/post-07" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-07</id>
    <updated>2026-07-07T00:00:00.000Z</updated>
    <published>2026-07-07T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">HRT 满一个月：用药方案、身体与情绪的变化，以及第 30 天的检验报告，一次坦诚的记录。</summary>
    <content type="html"><![CDATA[
<p>我是从6月1日正式开始HRT的。没有仪式，没有告知任何人，只是在那天早上像往常一样起身，然后做了个决定。我的用量如下表所示：</p>

























<table><thead><tr><th>时间段</th><th>用法</th></tr></thead><tbody><tr><td>1~7d</td><td>每日2mg雌二醇凝胶（早晚各4泵）+4*20mg螺内酯</td></tr><tr><td>7~14d</td><td>每日2mg雌二醇凝胶（早晚各4泵）+0.25*50mg醋酸环丙孕酮</td></tr><tr><td>14d~28d</td><td>每日4mg雌二醇凝胶（早晚各8泵）+0.125*50mg醋酸环丙孕酮</td></tr><tr><td>28d~至今</td><td>每日3mg雌二醇凝胶（早晚各6泵）+0.125*50mg醋酸环丙孕酮</td></tr></tbody></table>
<p>螺内酯后来被替换了，因为它的作用不够强，而且我总会忘记自己是否已经吃过。换用醋酸环丙孕酮之后，我知道自己每天只需要取四分之一片，不需要再数药片了。</p>
<h2>身体反应</h2>
<p>皮肤变化（定性分析）：皮肤出油减少了很多。以前到中午就会泛一层油光，现在一整张脸可以维持到傍晚不反光。身上的肉摸起来更软了，像松开的土壤。皮肤细腻程度和体毛没有明显变化，这两样是我想改变但暂时还没出现的部分。</p>
<p>腺体变化（定性分析）：乳腺在第23天开始有触痛感。那种感觉很奇怪，像皮肤下面有什么东西在松动。乳晕略微增大，颜色变得更浅了一些，不是特别明显，但我注意到了。</p>
<h2>精神反应</h2>
<p>食欲变化：总体上食欲增加了，但食物偏好也变了。以前吃东西是为了填饱肚子，现在会想吃甜的东西。小蛋糕、布丁、带糖浆的咖啡。</p>
<p>性欲变化：最后一次勃起反应记录在HRT第5天。之后就没有了。那种感觉像是某种长期存在的背景噪音被静音了。</p>
<p>精神医学：情绪变得比之前更可感。不一定是更高兴，但能更清楚地知道自己在难过，在高兴，或者只是平静。似乎开始从”知道自己在感受什么”慢慢变成”接受自己这样感受”。</p>
<h2>医学检验报告</h2>
<p>检验于HRT第30天。上午抽血，下午收到结果。数值比我预期的更高一些，但也不算意外。</p>





























<table><thead><tr><th>检验项目</th><th>英文缩写</th><th>检验结果</th><th>单位</th><th>参考范围</th><th>检验方法</th></tr></thead><tbody><tr><td>雌二醇E2</td><td>E2</td><td>↑319.36</td><td>pg/mL</td><td>0‑84 pg/mL</td><td>化学发光法</td></tr><tr><td>睾酮</td><td>TESTO</td><td>↓&lt;0.025</td><td>ng/mL</td><td>1.37‑8.25 ng/mL</td><td>化学发光法</td></tr></tbody></table>
<h2>一些还没有结论的感想</h2>
<p>一个月不长，但感觉已经过了很长时间。身体在慢慢变化，有些变化我已经能感知到，有些可能还需要更久才显现。我不知道这条路会不会通向我想去的方向，但我知道自己已经不想回到之前的生活了。</p>
<p>8月23日 第一次修改</p>]]></content>
    <category term="HRT" />
    <category term="身体记录" />
    <category term="随笔" />
  </entry>
  <entry>
    <title>节奏医生入门指南</title>
    <link href="https://www.refact.cc//posts/post-06" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-06</id>
    <updated>2026-03-22T00:00:00.000Z</updated>
    <published>2026-03-22T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">从校准到各类节拍，一份写给新手的《节奏医生》上手指南，尽是踩坑之后的经验总结。</summary>
    <content type="html"><![CDATA[
<p>好久不见，转眼两个月没写文章了。因为这个学期的课真的多到离谱，五天早六+五天基本满课</p>
<p>还有就是最近沉迷于《节奏医生》这款游戏。由7BG(7th Beat Games)开发，与《冰与火之舞》(adofai)同门。不同的是，这款游戏包含了大量剧情，有兴趣的可以去Steam购买游玩（原价58R）。这是一款单键音游，包含十余种节拍类型。虽然游戏玩法相对简单，只需要在第七拍按下即可，且对手速没有很高的要求。但是会存在同时治疗多位病人，窗口移动等视觉干扰，相比adofai会简单很多，但是又不会食之无味。</p>
<p>入门指南视频：<a href="https://www.bilibili.com/video/BV1ifAuzyEBM" rel="noopener noreferrer" target="_blank">Bilibili - BV1ifAuzyEBM</a></p>
<h2>校准（很重要）</h2>
<p>因为节奏医生这款游戏的判定标准相对其他音游是偏高的，只有good和bad两种评定，没有perfect、good等，比如喵斯快跑在±120ms内都认定为中了，而这是节奏医生简单难度的判定标准。所以在开始之前我们必须进行校准，这十分影响游戏体验。包含视效偏移和输入偏移。在第一次进行游戏时，伊恩医生会强制要求你进行校准，后续可以在设置中重复校准。</p>
<h3>视效偏移</h3>
<p>这通常是为了平衡渲染延迟（画面从 GPU 算完到显示的时间），一般电脑在8～25ms内。在看到第七个小方块闪烁时按下空格。</p>
<h3>输入偏移</h3>
<p>包含两部分，键盘输入延迟和耳机输出延迟。在听到第七拍时按下空格。</p>
<p>校准完成之后可以把设置-&gt;高级设置中的”显示判定数值”和”详细关卡结算”。开启”详细关卡结算”后，正常打一首歌，在显示评级时，你偏早和偏晚的帧数会显示在屏幕底部。理想情况下，偏早和偏晚的帧数应该是基本相同的，这样基本就校准好了。如果你后续更换了外设，如键盘、耳机等，都必须重新校准。</p>
<p>下表为有关难度配置的判定标准（1.0.5实际测试）</p>





























<table><thead><tr><th>难度</th><th>判定标准(ms)</th></tr></thead><tbody><tr><td>困难</td><td>±40</td></tr><tr><td>标准</td><td>±80</td></tr><tr><td>简单</td><td>±120</td></tr><tr><td>极简</td><td>±200</td></tr><tr><td>按了就中</td><td>—</td></tr></tbody></table>
<h2>节拍类型</h2>
<p>注：因为我也没什么乐理知识，所以就是经验总结，每个”-“代表等待间隔。</p>
<p>标准七拍子：以角色”武士”等为代表。第一关1-1（东方电舞曲）和1-X（东方不眠夜）最为典型，在第七拍按下即可。1-X包括1-XN因”网好卡菌”有严重的视觉干扰，需要必要的数拍。</p>
<p>标准二拍子：在第二章首次出现，代表为室上速病房的传奇音乐人”科尔”和”妮可”。2-2N（Unreachable）好听。此时护士会有”Rea—dy—ge—set—go”的节拍提示，100X100X（1为鼓点，0为空拍，在X处按下）即节拍间距。标准二拍子的第一拍和第二拍之间的间隔和上个第二拍的间距相同，匀速地按就好。</p>
<p>摇摆二拍子：也是以”科尔”和”妮可”为代表，2-3（缭绕），2-3N（嗅炸弹的博美犬），这里难度会升高很多，是标准二拍和摇摆二拍的混合，需要特别注意节拍转换。需要熟练掌握摇摆二拍子的技巧并多加练习方能通过。摇摆二拍子和标准略有不同，这里以2-3的节拍为例，护士的提示音会变为<strong>长 - 短 - 长 - 短</strong> ，所以每双数拍内的间隔会变短，双拍之间的间隔会变长。还有二拍子变速，在2-2的教程中出现，在2-X和2-XN(Bitter Times)会使用。</p>
<p>摇摆七拍子：在第三章出现，以”史蒂文森夫妇”和总统”理查德·休”为代表。3-2（典雅），3-3（分离二重奏）使用，拍型为1-2—3-4—5-6—7以及1—2-3—4-5—6-7两种类型。第一种在第六拍再等一拍，第二种在第六拍就要立即按了，这章总体难度不高。</p>
<p>长按拍子：在第四章来到了火车上，新人物”矿工大叔”。拍型为1—2-3—4-5—6-7—8（例），在第七拍按下，在第八拍松开。有时也和二拍子混合使用，如7-1（模糊）。要注意提示”咔”的一声，不能短按，这种拍型会在以后的章节频繁出现。</p>
<p>细分二拍子：一种新型的二拍子，在第五章（理疗病房）首次出现，以棒球手”路吉”为代表，5-1（路吉摔了一跤）为准。一般一次以两个及以上出现，护士会用英文提示”Two!”，然后砰+（细分拍长）+砰+（拍长）+啪+（细分拍长）+啪。细分拍长就是把原本 1 拍的时间，均匀拆成 2 小拍，按 2 次按键。在啪声处按下即可，在主线剧情中，最多的是八连拍（7-X2）。</p>
<p>冻结拍子：在5-2(Lo-fi Beats For Patients To Chill To)出现，音效（两节，提示音在第一节出现）提示为铿+（冻结拍长）+咔+（拍长）+啪+铿+（拍长）+（冻结拍长）+啪。需要注意的是，因为上一节的拍长变化，所以下一节需要提前一个冻结拍长按键，这是比较难的地方。</p>
<p>灼热拍子：在且仅在5-2N（无以为继的伤心欲绝）中出现，与冻结拍子相反，需要在第二拍的拍长减去灼热拍长，而5-2N将冻结和灼热拍子相结合（森林冰火人），难度很高，需要背板才能打下来。</p>
<p>冲击拍子：在5-X（梦想不停歇）中首次出现，在听到哨声时就意味着要出现冲击拍子了，哨声之间的间隔为标准拍长，所以每一拍都要按键，难点是前后的衔接和匀速按键。</p>
<p>切分拍子：第六章我们来到了病历间，在6-1（想把心事和你说）首次出现，在轨道上显示的是并列的三角，伴有提示音”唧”，一般是在标准七拍中掺入，在经过这个拍子时，拍长会变为原本的一半，需要提前按键，尤其是在第六拍切分时。</p>
<p>五拍子：在主线剧情中仅在患者”马克·布克”身上出现（即6-2，6-X，7-1），因为先前遭受的心理创伤，他的轨道变短了，需要在第五拍按下，所以这类关卡的按键频率会高一些。</p>
<p>还有其他拍子如摇摆五拍子（5-2），分裂三连音的二拍子（6-X）等就不展开介绍了，可以在教程中反复练习以上所有种类的拍型。</p>
<h2>社区关卡</h2>
<p>除了官方制作的主线关卡之外，游戏内置的关卡编辑器允许玩家制作自定义关卡，可以在Steam创意工坊和 <a href="https://rhythm.cafe/" rel="noopener noreferrer" target="_blank">https://rhythm.cafe/</a> 获取这些社区关卡。社区关卡和官方关卡相比，可能存在不符合标准的提示音和过多的视觉干扰，导致几乎无法读谱。</p>
<h2>参考文献</h2>
<p>Bilibili-春望之人BEP：<a href="https://space.bilibili.com/179800587/lists/3844590" rel="noopener noreferrer" target="_blank">https://space.bilibili.com/179800587/lists/3844590</a></p>
<p>Rhythm Doctor Wiki：<a href="https://wiki.rhythm.cafe" rel="noopener noreferrer" target="_blank">https://wiki.rhythm.cafe</a></p>]]></content>
    <category term="音游" />
    <category term="节奏医生" />
    <category term="攻略" />
  </entry>
  <entry>
    <title>从 JavaScript 前端开发到 Electron 框架跨平台开发</title>
    <link href="https://www.refact.cc//posts/post-05" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-05</id>
    <updated>2026-01-02T00:00:00.000Z</updated>
    <published>2026-01-02T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">从 JavaScript 的诞生讲到 Electron 的跨平台实践，梳理二者的关系，以及它们对软件开发与原生开发格局的影响。</summary>
    <content type="html"><![CDATA[
<p><strong>关键词</strong>：全栈开发、JavaScript、Electron等。</p>
<p><strong>引言</strong> ：要说互联网网页的三大基石，那毋庸置疑是HTML、CSS、JavaScript。其中控制整个页面逻辑的就是JavaScript，是进行人机交互不可或缺的接口。而最初的JavaScript只能在浏览器中运行，也就是说，那时的JavaScript只能作为一个前端语言来使用。而之后，Node.js 横空出世，使得JavaScript可以作为一门后端语言来使用。此外，使用Electron 框架可以通过前端语言来创建跨平台应用，囊括了 Windows、macOS、Linux。</p>
<h2><strong>什么是JavaScript？(1)</strong></h2>
<p>实际上，JavaScript 和 Java 没有实质上的关系，甚至可以说是毫不相干的。</p>
<p>当时，主流的浏览器之一是网景浏览器（Netscape Navigator），网页主要由超文本标记语言（HTML）构成，交互能力非常有限。用户在浏览网页时大多只能被动查看，主要的交互操作仅能通过点击超链接实现，这显然有很大的局限性。</p>
<p>因此，在 1995 年 9 月，JavaScript（最初称为 Mocha，后改名 LiveScript，最终定为 JavaScript）被引入，它提供了一系列可在页面内执行的脚本功能。这些脚本通过 <code>&lt;script&gt;</code> 标签嵌入 HTML，能够在页面加载或用户交互时运行。其核心思想是实现动态的页面行为，类似于如今局部刷新的交互模式，例如：通过浏览器 API 弹出对话框、控制页面滚动，或是读取浏览器提供的部分信息（如用户代理、操作系统等），并基于这些信息执行相应逻辑。</p>
<p>当然，早期的 JavaScript 并不完善，存在诸多兼容性与功能上的问题，需要各个浏览器厂商逐步实现统一的标准支持。</p>
<p>JavaScript 的标准化进程由 ECMA International 组织推进，1997 年首次发布 ECMAScript 标准。2009 年发布的 ES5 是重要的里程碑，提升了语言稳定性与功能性。与此同时，2008 年 Google 发布的 Chrome 浏览器搭载了 V8 引擎，极大提高了 JavaScript 的执行性能。随着现代浏览器对标准更加一致的支持，基于 JavaScript 的生态系统迅速发展壮大，其中包括服务器端的 Node.js（2009 年发布）以及桌面应用框架 Electron（2013 年发布）等，进一步拓展了 JavaScript 的应用领域与社区影响力。</p>
<h2><strong>什么是Electron框架？(2)</strong></h2>
<p>Electron 最初由 GitHub（在微软收购之前）开发，是一个基于 Node.js 和 Chromium 的开源应用程序框架。该框架本身主要由 C++ 编写，其核心原理是将网页（前端技术）与 Node.js 后端能力结合，封装成一个独立的桌面应用程序。Electron 本身提供了一系列原生 API，使开发者能够与操作系统进行交互和调用系统功能。</p>
<p>这一点与 Windows 10 时期引入的 Web UI（如 UWP (4)中使用 WebView 的界面）不同，因为两者所处的架构层次和权限级别存在差异。Windows 系统自身的 UI 组件是在系统层面统一实现和管理的，同一组件在不同应用间行为一致，且通常由操作系统直接调度与绘制。而 Electron 则允许通过其底层接口调用操作系统提供的原生 UI 元素，例如窗口、对话框、菜单等，并可以深入访问系统级功能。</p>
<p>Chromium 在 Electron 中主要负责网页的渲染、网络请求的处理（如解析与发送数据包），以及提供现代的浏览器能力。其底层通过 C++ 实现，使得 Electron 应用能获得比一般 Web 应用（如运行在浏览器沙盒环境中的 Web UI）更高的系统资源访问权限。</p>
<p>当然，这种架构也存在一些不足。由于每个 Electron 应用都内置了完整的 Chromium 渲染引擎和 Node.js 运行时，其内存占用通常比纯粹的原生应用更高，这也是其常见的性能权衡之一。</p>
<p>目前，已经有不少知名软件采用 Electron 来实现跨平台开发，例如微软的 Visual Studio Code、腾讯的 QQ NT 以及 Discord 等。这些应用在保持良好跨平台兼容性的同时，也提供了丰富的现代交互体验。</p>
<h2><strong>这是如何实现的？(5)</strong></h2>
<p>·   <strong>Chromium</strong>：用于渲染用户界面，每个窗口都是一个独立的 Chromium 渲染进程，支持现代 Web 标准。</p>
<p>·   <strong>Node.js</strong>：在主进程中运行，提供文件系统、网络等操作系统 API 的访问能力。</p>
<p>而Electron 框架就是起到了一个桥接的作用，将Web层面的操作翻译给后端处理，再将处理后的结果传给前端。</p>
<p>而对于系统API的调用，内存管理，文件操作等大多是由后端的Node.js处理的。在实际软件开发中，需要用类似 Electron Forge 的工具，将软件打包为 tar.gz(Linux)/dmg(macOS)/exe(Windows)来进行后续的分发操作。</p>














<table><thead><tr><th>Web 技术层 (HTML/CSS/JS)</th></tr></thead><tbody><tr><td><code>Electron 桥接层</code></td></tr><tr><td><code>Chromium + Node.js 运行时环境</code></td></tr></tbody></table>
<p>我们知道，不同的操作系统的操作有所差异比如Windows上的Alt键对应着macOS的Option键，文件系统目录<code>/Macintosh HD/Application Supports</code>（macOS）对应了<code>C:\Program Files</code>(Windows)，所以Electron程序会根据所打包的程序类型转换按键绑定和文件系统定位等，而开发者也需要在各个平台进行充分测试和调试。</p>
<h2><strong>对于软件开发的影响力</strong></h2>
<p>为什么选择 Electron？在进行软件开发时，开发者必须考虑其多平台兼容性，如果应用程序不支持目标用户的操作系统，会直接导致客户的流失。而对于主流平台独占的软件开发框架，如dotnet(Windows)、Swift（macOS）、GTK（Linux）。这种专业性要求和学习成本极高的软件开发策略显然不是最优解。</p>
<p>而HTML、CSS、JavaScript作为前端开发工程师必备的技能，只需要相对较低的学习成本就可以开发三端互通的应用程序。所以近年来，使用Electron 框架的公司数量也在稳步增长。</p>
<h2><strong>那原生开发怎么办？</strong></h2>
<p>我们一开始就提到了，相比于原生开发，Electron也是存在弊端的，这点在官方文档（5）也有提及。</p>
<p>首先是在资源受限的环境，我们之前提到了，由于软件的运行需要多一层翻译，所以转换效率肯定不如原生语言直接输出到显示器。也包括内存资源的占用。这在类似物联网设备上是绝对的红线。</p>
<p>其次，Electron 通过将最新版本的 Chromium、V8 和 Node.js 直接与应用程序二进制文件打包在一起，安装包自带浏览器内核。所以，打包后的包体一般不会小于100MiB。即使在操作系统有内嵌浏览器的情况下，据开发者所述（4），这是为了稳定性、安全性和性能。因此，对于一些磁盘占用敏感的情况下，我们不得不选择原生开发，因为可以调用系统自带的API。</p>
<p>再者，显而易见，Web技术并不适合用于编写游戏。因为往往Unity、虚幻引擎、DirectX/OpenGL 等原生框架可以提供更出色的性能，并能更直接地访问图形硬件。</p>
<h2><strong>参考文献</strong></h2>
<p>1.维基百科 - <a href="https://zh.wikipedia.org/wiki/JavaScript" rel="noopener noreferrer" target="_blank">https://zh.wikipedia.org/wiki/JavaScript</a></p>
<p>2.维基百科 - <a href="https://zh.wikipedia.org/wiki/Electron" rel="noopener noreferrer" target="_blank">https://zh.wikipedia.org/wiki/Electron</a></p>
<p>3.Electron 官方文档 - <a href="https://www.electronjs.org/zh/docs/latest" rel="noopener noreferrer" target="_blank">https://www.electronjs.org/zh/docs/latest</a></p>
<p>4.WIKIPEDIA - <a href="https://en.wikipedia.org/wiki/Universal_Windows_Platform" rel="noopener noreferrer" target="_blank">https://en.wikipedia.org/wiki/Universal_Windows_Platform</a></p>
<p>5.Electron 官方文档 - <a href="https://www.electronjs.org/zh/docs/latest/why-electon" rel="noopener noreferrer" target="_blank">https://www.electronjs.org/zh/docs/latest/why-electon</a></p>
<p>12月19日 起稿</p>
<p>1月2日 初稿</p>
<p>1月3日 第一次修改</p>]]></content>
    <category term="JavaScript" />
    <category term="Electron" />
    <category term="跨平台" />
  </entry>
  <entry>
    <title>图床配置指南</title>
    <link href="https://www.refact.cc//posts/post-04" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-04</id>
    <updated>2025-12-30T00:00:00.000Z</updated>
    <published>2025-12-30T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">基于 Cloudflare R2 与 Worker 搭建私有图床，并让 Waline 评论区支持图片上传的完整配置指南。</summary>
    <content type="html"><![CDATA[
<p>注意：启用图床将会禁用图片优化功能。如果你强制在<code>./astro.config.mjs</code>的52行处修改为你的图床地址，那大概会被防盗链功能拦截，所以图片优化仅限静态资源。</p>
<p>首先需要的是S3对象存储，任何提供商理论上都支持，由于涉及到接口的编写，所以以CF R2和Worker举例。</p>
<p>本方法来自 <a href="https://tbbbk.com/archives/cloudflare-r2-imgbed" rel="noopener noreferrer" target="_blank">https://tbbbk.com/archives/cloudflare-r2-imgbed</a> 但其中的内容稍有纰漏，所以我决定重写这一部分。</p>
<h2>开通对象存储</h2>
<p>CF主页 &gt; 存储和数据库 &gt; R2对象存储 &gt; 概述 &gt; 创建储存桶</p>
<p>名称为 imgs ，位置选亚太地区，标准类型。</p>
<h2>配置Worker</h2>
<p>CF主页 &gt; 计算和AI &gt; Workers 和 Pages &gt; 创建应用程序</p>
<p>选择从 Hello World! 开始，Worker name填 imgctl ，然后部署。</p>
<p>进入创建好的 Worker ，编辑代码。</p>
<p>Cloudflare worker 配置如下：</p>
<p>你需要在allowedDomains块中引入你的源域名，也就是你需要从哪里访问这些资源。</p>
<pre><code>export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const pathname = url.pathname;

    const referer = request.headers.get('Referer') || '';
    const allowedDomains = ['refact.cc', 'www.refact.cc', 'xlog.app', 'refact.xlog.app', 'localhost'];

    // -----------------------------
    // ✅ CORS 预检
    // -----------------------------
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Max-Age': '86400'
        }
      });
    }

    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization'
    };

    // -----------------------------
    // ✅ POST 上传（使用 Token 校验）
    // -----------------------------
    if (request.method === 'POST' &amp;&amp; pathname === '/upload') {
      try {
        const auth = request.headers.get('Authorization');
        if (!auth || auth !== `Bearer ${env.UPLOAD_TOKEN}`) {
          return new Response(
            JSON.stringify({ error: 'Invalid upload token' }),
            { status: 403, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
          );
        }

        const formData = await request.formData();
        const file = formData.get('file');

        if (!file) {
          return new Response(
            JSON.stringify({ error: 'No file uploaded' }),
            { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
          );
        }

        // 1. 获取 URL 参数 ?path=xxx
        const pathType = url.searchParams.get('path');

        // 2. 生成基础随机文件名
        const ext = file.name.substring(file.name.lastIndexOf('.')) || '';
        const uniqueName = `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`;

        // 3. 根据参数决定最终路径
        let filename;
        if (pathType === 'root') {
          // 如果参数是 root，直接放在根目录 (Admin 上传)
          filename = uniqueName;
        } else {
          // 默认情况，放在 comment 文件夹下 (Waline 评论)
          filename = `comment/${uniqueName}`;
        }

        const buffer = await file.arrayBuffer();

        await env.MY_BUCKET.put(filename, buffer, {
          httpMetadata: { contentType: file.type }
        });

        return new Response(
          JSON.stringify({ url: `https://img.refact.cc/${filename}` }),
          { status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
        );
      } catch (err) {
        return new Response(
          JSON.stringify({ error: err.message }),
          { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
        );
      }
    }

    // -----------------------------
    // ✅ GET 图片（Referer 防盗链）
    // -----------------------------
    if (request.method === 'GET') {
      let allowed = false;
      
      // 如果路径以 /base/ 开头，直接允许，不检查 Referer
      if (pathname.startsWith('/base/')) {
        allowed = true;
      } 
      else {
        // 否则，执行常规的 Referer 检查
        for (const domain of allowedDomains) {
          if (referer.includes(domain)) {
            allowed = true;
            break;
          }
        }
      }
      

      if (!allowed) {
        // 如果想允许直接浏览器访问（无 Referer），可以把下面的注释打开
        // if (!referer) allowed = true; 

        if (!allowed) {
          const warning = await env.MY_BUCKET.get('hotlink-warning.png');
          if (warning) {
            return new Response(warning.body, {
              status: 200,
              headers: {
                'Content-Type': 'image/png',
                'Cache-Control': 'public, max-age=3600',
                'X-Robots-Tag': 'noindex'
              }
            });
          }
          return new Response('Hotlinking not allowed', {
            status: 403,
            headers: { 'Content-Type': 'text/plain', 'X-Robots-Tag': 'noindex' }
          });
        }
      }

      const key = pathname.slice(1);
      if (!key) return new Response('Bad Request', { status: 400 });

      const object = await env.MY_BUCKET.get(key);
      if (!object) return new Response('Not Found', { status: 404 });

      const headers = new Headers();
      object.writeHttpMetadata(headers);
      headers.set('etag', object.httpEtag);
      headers.set('Cache-Control', 'public, max-age=31536000, immutable');
      headers.set('Access-Control-Allow-Origin', '*');

      return new Response(object.body, { status: 200, headers });
    }

    return new Response('Method Not Allowed', { status: 405 });
  }
};

</code></pre>
<p>接下来绑定储存桶，添加绑定。选择R2 储存桶。变量名称必须填 MY_BUCKET 。R2 储存桶选imgs 。</p>
<p>下一步绑定域名，设置 &gt; 域和路由，你需要添加一个自定义域，方便在文章中引入。</p>
<p>转到设置 &gt; 变量和机密，你还需要加入一个名为UPLOAD_TOKEN的环境变量</p>













<table><thead><tr><th>变量名</th><th><code>UPLOAD_TOKEN</code></th></tr></thead><tbody><tr><td>值</td><td>自行设置，复杂一些，如果不使用Waline图片上传，可以忽略这一步。</td></tr></tbody></table>
<h3>配置PicGo或PicList</h3>
<p>在这两款软件中，驱动选S3对象存储就可以。</p>
<p>CF主页 &gt; 计算和AI &gt; Workers 和 Pages &gt; 概述</p>
<p>在Account detials 中 API Token栏右侧的 Manage按钮，创建 Account API 令牌，名称自拟，权限是对象读和写，其他保持默认。</p>
<p>复制Access Key ID 和 Secret Access Key（只显示一次），桶名就填imgs, 设定自定义节点，就填创建令牌时显示的S3 API。概述页最底部显示的内容。你还可以自定义上传路径。其他配置保持不变。</p>
<p>本文描述相对简洁，如果有疑问可以去看原文，如果问题是由本项目导致的，请提交issues。</p>
<h2>在Waline 评论系统中启用图片上传</h2>
<p>在./src/config.ts 配置文件的末尾，是有关waline评论系统的配置项，其中新增了三个配置项：是否启用图片上传功能、Waline 图片上传地址和Waline 上传 Token。这三个选项必须同时设置。</p>
<p>Waline评论上传的图片默认统一保存在comment文件夹下。</p>
<p>根据Worker的默认配置，图片上传地址应为<a href="https://img.example.com/upload" rel="noopener noreferrer" target="_blank">https://img.example.com/upload</a> 注意不要填错了。至此，配置完成，你在写文章时，通过PicGo/PicList上传至数据库，然后在Markdown文件中按照和以前一样的格式从图床引用就好。</p>
<pre><code>// 带描述的图片
![lighthouse](https://img.refact.cc/lighthouse.png)
</code></pre>
<p>更多用法，请到 <a href="https://refact-x-template-git-galaxy-msrefs-projects.vercel.app/posts/markdown-extension-syntax" rel="noopener noreferrer" target="_blank">https://refact-x-template-git-galaxy-msrefs-projects.vercel.app/posts/markdown-extension-syntax</a> 学习。</p>
<h2>外部访问提示</h2>
<p>你可以在存储桶的根目录放置一张提示图,命名为<code>hotlink-warning.png</code>，大概就是提示禁止从外部访问，不放也会有相应的文字提示。</p>
<p>12月26日 起稿</p>
<p>12月30日 初稿</p>
<p>1月4日 第一次修改</p>
<p>2月12日 第二次修改</p>
<p>4月10日 第三次修改</p>]]></content>
    <category term="图床" />
    <category term="Cloudflare" />
    <category term="Waline" />
  </entry>
  <entry>
    <title>2025年度总结</title>
    <link href="https://www.refact.cc//posts/post-03" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-03</id>
    <updated>2025-12-28T00:00:00.000Z</updated>
    <published>2025-12-28T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">第一次写年度总结：从几次建站的技术成长，到军训与宿舍里的大学生活，以及那些说不出口的无力感。</summary>
    <content type="html"><![CDATA[
<p>这是我第一次写年度总结。</p>
<p>总的来说，今年是特殊的一年，而这个博客也是从今年7月26日才开始运行的。而也是从今年年初，我才开始系统性地的学习前端技术，现在开始自称开发者确实有点名不副实了，但我还是会不断学习下去的。</p>
<h2>时间线</h2>
<h3>今年二月中旬-偶遇NotionNext</h3>
<p>偶然间发现 tangly1024 大佬开发的 NotionNext 项目，核心是”零基础建站”，我对此深感兴趣。于是便按照教程一步步地创建了第一个博客，主要就是Minecraft的内容，没有什么有价值的产出。</p>
<p>而我也借此机会学习了一部分tailwind CSS样式修改，即现在的Reborn仓库。</p>
<h3>今年四月初-第一次独立项目</h3>
<p>我创建了一个名为MSW(Minecraft status widget)的项目，使用的是Next.js框架，虽说都是AIGC，不过我也认识了Next.js的基本结构，这比看文档有趣多了。</p>
<h3>今年五月中旬-开始使用Vitepress</h3>
<p>这是我第一次构建一个静态站点。从一个模版开始，也渐渐熟悉了基本的CRUD操作，不过我还有很多东西需要学习。</p>
<h3>今年六月至七月-了解xLog</h3>
<p>确实很方便，但那并不自由。</p>
<h3>今年七月中旬-第二次建站</h3>
<p>高考后在家昏迷了一个星期，后续到这个时间点之前，我都在对之前写的项目进行维护，写点文档什么的（虽然没人看）。</p>
<p>我发现Astro网站上有很多博客模版可用，于是就选了一个<a href="https://github.com/kieranwv/astro-theme-vitesse" rel="noopener noreferrer" target="_blank">https://github.com/kieranwv/astro-theme-vitesse</a> 作为主题，但后续我发现这个主题扩展性有限，还有一些不可名状的显示Bug，虽然做了很多修改，但感觉效果还是差强人意，于是就渐渐不再维护这个项目了。</p>
<h3>今年7月26日至今-第三次建站</h3>
<p>如果你能看到这篇文章，那你所见的就是我称作”RefactX Project”的项目，它是基于<a href="https://github.com/Dnzzk2/Litos" rel="noopener noreferrer" target="_blank">https://github.com/Dnzzk2/Litos</a> 修改得来的。有关这个项目的更多信息，你可以去看第一篇文章。</p>
<h3>总结</h3>
<p>总的来说，我将大部分时间花在了这个项目上，以后也会继续边学习边维护下去。因为迁移的成本实在是太高了。</p>
<h2>大学生活</h2>
<p>首先就是军训，其实有关这部分的内容我在十月初就已经写出来了，但是一直没有发出来。</p>
<div><div><div></div><div>NOTE</div></div><div><p>我并不是有意放大学校的问题所在，更多的是对事实的阐述和揭露那些不合理的制度。</p></div></div>
<h3>军训</h3>
<p>军训是对人身体和精神上的双重折磨，给人更多的是一种无力感。</p>
<p>军训的核心就是<strong>服从命令</strong>，这四个字是贯穿了整个20天军训的始终的。同时又伴有一些完整的，有计划的体罚制度，让你不得不去服从他人的命令。</p>
<p>早上六点半上训，这意味着需要六点起床，实际上我每天很少睡够五个小时。一天总共训练九个小时左右，反正就是很忙，就是不知道在忙些什么。</p>
<img src="https://img.refact.cc/8-1.jpeg" alt="军训" />
<h3>反抗？</h3>
<p>这就不得不提到这个墨守成规的连坐制度了，不管你是不是故意的，他们都会把个人层面的事上升到群体层面，连带着你们的同学一起惩罚。</p>
<p>这种制度不仅仅激化了学生与教官的矛盾，也激化了学生之间的矛盾，是极不利于团结的，处罚后的”有效”现象，更大程度上是出于对处罚的畏惧罢了。</p>
<p>就在这种制度之下，队伍里当然会出现不和谐的声音，如果这种声音被教官听到了，那么迎来的将是更严重的惩罚，最后学生的身体会受到损害，而这就是反抗的后果。</p>
<p>我真的很累很累，这是军训给我留下的唯一的感受。</p>
<img src="https://img.refact.cc/8-2.jpeg" alt="军训" />
<h2>欺骗</h2>
<p>河南农村的小孩从小就一直被灌输”上了大学就轻松了”的思想，于是和一百多万考生厮杀，不足一半的学生踏入了梦寐以求的本科院校。然而到了大学之后一切的幻想都化作泡影。</p>
<h3>环境</h3>
<p>而这种感受其实在开学那天就发现了。我的宿舍是混合的，并且不是同一专业，甚至不是同一年级的，他们是土木工程的二年级学生。这也间接导致了我到现在还认不出我们班的哪怕一个学生，我也因此总是一个人行动。</p>
<p>我是在系统一开放就选了的，然而我当时可选的四个宿舍全是混合的，我没得选。</p>
<p>我们宿舍看上去也是很有年代感，包浆级别的，我擦了十几张湿巾还是黑的。没有独立卫浴，只有走廊尽头的洗漱间，包括澡堂。还有很多人根本就不冲厕所，恶心别人。</p>
<p>舍友不喜欢戴耳机，总是外放，说话很大声。打游戏时还会大吼大叫，降噪耳机根本没用。每到这时，深深的无力感再次席卷而来。</p>
<h3>教学</h3>
<p>根据教学内容和考核方式，课程被学生选出了一类”水课”，也就是相对不那么重要的课。在这类课程中，学生大多不愿意认真听讲，在使用手机、跑神、或是直接睡觉，于是老师也不再认真讲课了，都是为了完成既定的教学任务罢了。</p>
<h3>数学</h3>
<p>如果你看过我之前的文章，就知道数学是我的绝对劣势，所以说我现在上数学课总是在一个小本上写写画画。</p>
<img src="https://img.refact.cc/8-4.jpeg" alt="数学" />
<h3>排球课</h3>
<p>这学期的体育大概率也是要挂科了。我选的是排球，当时选课系统上也确实没什么可选了。直到练了几天之后发现还是一个都垫不出去，于是我的搭档也就被我赶走去找别人练了。前些天测试，因为我没有搭档，所以自然没有成绩。1000米那天我发烧没去，所以也没有成绩。至于校园跑，我们学校规定是140公里，当然大部分学生是靠骑电动车和自行车刷的。每个人都心知肚明，我写这篇文章的时候，我的校园跑数据是六公里。</p>
<img src="https://img.refact.cc/8-3.jpeg" alt="排球课" />
<h2>以后呢？</h2>
<p>直到我写这篇文章的时候，我仍不认识宿舍内的哪怕任何一个同学。</p>
<p>大概率是不会有变化了，因为我已经开始厌恶这个地方了。</p>
<p>同时我也是一个不喜欢受规划所束缚的人，以后的事情现在暂时不做考量。</p>
<p>2025年12月25日起稿</p>
<p>2025年12月28日初稿</p>]]></content>
    <category term="年度总结" />
    <category term="大学生活" />
    <category term="随笔" />
  </entry>
  <entry>
    <title>校园网有线多设备上网方案</title>
    <link href="https://www.refact.cc//posts/post-02" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-02</id>
    <updated>2025-11-28T00:00:00.000Z</updated>
    <published>2025-11-28T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">从直连、桥接到 PPPoE 拨号，记录一次与校园网单设备限制的较量，最终让所有设备共享同一条有线网络。</summary>
    <content type="html"><![CDATA[
<h2>概述</h2>
<p>我有很多台网络设备，手机、电脑、打印机、Debian服务器、其他Mesh设备等等需要联网使用。但是无线校园网仅支持一台设备在线，包括我在<code>pnpm dev --host</code>时也不能通过内网连接的手机实时调试网页在移动端的显示效果。于是我必须得想办法解决这个瓶颈。在网络上检索相关的教程，但其有效内容甚少，并无一个成体系的解决方案，甚至上升到需要刷openwrt或者老毛子固件，或者去购买专门的的路由器（非常贵）。最后我还是尝试自己解决。</p>
<h3>无线校园网</h3>
<p>我使用的是电信校园卡附带的校园网服务包。其<strong>无线网络</strong>通过内网Web页面认证学号和密码（DHCP + Web Portal）。经过测试，这种方案仅支持一台设备在线（MAC地址克隆无效）。每次连接都需要输入帐密认证身份。</p>
<h3>有线接入点（AP）</h3>
<p>我们的接入点是在宿舍门的上方，有五个RJ45的网口。花费8元购置一根十米超五类网线，完成物理布线。</p>
<h2>尝试</h2>
<h3>直连电脑</h3>
<h4>DHCP直连</h4>
<p>无法正常分配IP地址，说明AP并不是通过DHCP + Web Portal连接的。</p>
<h3>连接路由器</h3>
<p>这个路由器是我在并少少买的28块的二手中兴Z500。</p>
<h4>桥接(LAN)</h4>
<p>将路由器设置为桥接/AP模式后，电脑连接到路由器LAN口。电脑依然无法从校园网DHCP服务器获取到IP地址。这证明网络接入层面（而非仅Web认证层面）就对设备或拓扑进行了限制（很可能基于接入端口的MAC地址绑定）。退一步说，即使桥接成功，该模式也无法实现我所需的”单一入口、多设备共享”的NAT转换功能。</p>
<h4>DHCP(WAN&amp;LAN)</h4>
<p>无法识别WAN口和LAN口网线。</p>
<p>后来从运营商处了解到有线网络并不是通过DHCP + Web Portal认证的，不管是电脑还是路由器，都需要（学号）+@telecom这个账号PPPoE拨号上网。</p>
<h4>PPPoE(WAN)</h4>
<p>ISP连接超时。（未知错误）或认证失败。我去运营商处咨询后得到正确的帐密登陆。成功连接。</p>
<h3>新问题</h3>
<p>连接速度慢的离谱，通过日志可以看出是卡在分配IP地址这一步。查阅资料发现我在某个教程的指引下（似乎是桥接方法那里）关闭了DHCP服务器，当DHCP过程失败时，操作系统在等待DHCP响应超时后，会<strong>自动启用一个备用机制</strong>，即给自己分配一个 <strong>“链路本地地址”</strong>（这个地址无法上网）。重新启用DHCP服务器即可。</p>
<h2>结语</h2>
<p>我终于摆脱了每次打开电脑输入密码的时代。</p>
<img src="https://img.refact.cc/post-6-1.png" alt="多设备连接" />
<img src="https://img.refact.cc/post-6-2.jpg" alt="5GHz频段测速" />
<p>我不理解学校的这种行为，我们学校的可分配网段应该是10.209.0.0/16.也就是说足足可分配65025个IP地址，即使全校学生同时上网也是完全够用的。我们学校对学生的限制已经够多了，所以这次我必须打破这个规则。</p>
<p><strong>总936词</strong></p>
<p>2025年11月28日 初稿</p>
<p>在 Typora | macOS Tahoe 26.1 编辑</p>]]></content>
    <category term="校园网" />
    <category term="网络" />
    <category term="路由器" />
  </entry>
  <entry>
    <title>Waline评论系统的部署与配置</title>
    <link href="https://www.refact.cc//posts/post-01" rel="alternate" type="text/html"/>
    <id>https://www.refact.cc//posts/post-01</id>
    <updated>2025-09-26T00:00:00.000Z</updated>
    <published>2025-09-26T00:00:00.000Z</published>
    <author>
      <name>refac7</name>
    </author>
    <summary type="text">从后端部署到前端接入，一步步记录 Waline 评论系统的搭建过程，涵盖数据库、邮件通知与常见坑。</summary>
    <content type="html"><![CDATA[
<p>本文使用的是前后端分离的方式，当然可以使用完全嵌入的方式，不过我没成功。</p>
<h2>后端配置</h2>
<p>推荐使用 vercel 无服务器部署，使用<code>@waline/vercel</code> 包进行部署</p>
<p><a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwalinejs%2Fwaline%2Ftree%2Fmain%2Fexample" rel="noopener noreferrer" target="_blank">https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwalinejs%2Fwaline%2Ftree%2Fmain%2Fexample</a></p>
<p>点击以上链接跳转到 vercel 进行部署，如果未注册，可使用GitHub账户注册。</p>
<h3>添加数据库环境变量</h3>
<p>除了官方推荐的 Leancloud 之外，也支持 MySQL, PostgreSQL, SQLite 以及 MongoDB。</p>
<p>相关环境变量键值可以在官方文档查看 <a href="https://waline.js.org/guide/database.html" rel="noopener noreferrer" target="_blank">https://waline.js.org/guide/database.html</a></p>
<div><div><div></div><div>WARNING</div></div><div><p>官方提示 MySQL, PostgreSQL, SQLite 数据库要提前创建表结构</p></div></div>
<p><a href="https://github.com/walinejs/waline/blob/main/assets/waline.sqlite" rel="noopener noreferrer" target="_blank">waline.sqlite</a></p>
<h3>部署和后续工作</h3>
<p>通过 vercel 部署完成后，需要绑定自己的域名使用，因为 vercel.app 在大陆不可访问。在此之后，你才可以直接访问，并且看到评论窗口，然后你可以直接发送一个实例评论，然后在数据库查看是否写入键值。</p>
<h2>前端配置</h2>
<p>对于未集成评论系统的静态博客系统，我们一般要先创建一个 泛用组件，然后在博客页模版相应位置引入即可。</p>
<p>下面我给出一个 Astro静态博客的示例组件</p>
<pre><code>// /src/components/posts/base/Comments.astro
&lt;div id="waline"&gt;&lt;/div&gt;

&lt;link
  rel="stylesheet"
  href="https://unpkg.com/@waline/client@v3/dist/waline.css"
/&gt;

&lt;style is:global&gt;
  /* 去掉 Waline 所有圆角 */
  #waline .wl-editor,
  #waline .wl-input,
  #waline span.wl-badge,
  #waline .wl-btn,
  #waline .wl-comment,
  #waline .wl-card,
  #waline .wl-panel,
  #waline .wl-header,
  #waline .wl-gif-popup,
  #waline .wl-emoji-popup,
  #waline .wl-footer {
    border-radius: 0 !important;
    font-family: 'ShangguSansSC Variable', system-ui, -apple-system, sans-serif !important;
  }

  .wl-content,
  .wl-content * {
    white-space: normal !important;
    word-break: break-word !important;
    overflow-wrap: anywhere !important;
  }

  /* 浅色模式下的颜色设置 */
  #waline {
    --waline-theme-color: #000000 !important;
    --waline-active-color: #dc2626 !important;
    /* 设置字体 */
    --waline-font-size: 1rem !important;
    --waline-font-family: 'Onest Variable', 'Geist Variable', 'ShangguSansSC Variable', system-ui, -apple-system, sans-serif !important;
  }

  /* 深色模式下的颜色和背景设置 */
  html.dark #waline {
    --waline-theme-color: #ffffff !important;
    --waline-active-color: #dc2626 !important;
  }

  html.dark #waline .wl-panel {
    --waline-bgcolor: #101018 !important;
    background-color: #101018 !important;
  }

  /* 禁用输入框获得焦点时的样式变化 */
  #waline .wl-editor:focus,
  #waline .wl-editor:focus-within,
  #waline .wl-input:focus {
    background-color: var(--waline-bgcolor) !important;
    box-shadow: none !important;
    outline: none !important;
  }
&lt;/style&gt;

&lt;script src="https://unpkg.com/@waline/client@v3/dist/waline.umd.js" defer&gt;&lt;/script&gt;
&lt;script&gt;
function loadWaline() {
  const container = document.querySelector('#waline');
  if (!container) return; // 容器不存在就不初始化

  const w = (window as any).Waline;
  if (!w) return;

  // 如果已有实例，先安全销毁
  const oldInstance = (window as any).__walineInstance;
  if (oldInstance &amp;&amp; typeof oldInstance.destroy === 'function') {
    try {
      // 只有当挂载点还在 DOM 中时才销毁
      if (container.contains(container.firstChild) || container.childNodes.length &gt; 0) {
        oldInstance.destroy();
      }
    } catch (e) {
      console.warn('Waline destroy failed:', e);
    }
  }

  // 重新初始化并保存实例
  (window as any).__walineInstance = w.init({
    el: container,
    serverURL: 'https://waline.refact.cc/',
    path: location.pathname.replace(/\/$/, ''),
    lang: 'zh-CN',
    pageview: true,
    dark: 'html.dark',
    requiredMeta: ['nick', 'mail'],
    locale: { placeholder: '写点什么吧…' }
  });
}


  // 首次加载
  window.addEventListener('DOMContentLoaded', loadWaline);

  // PJAX 完成后
  document.addEventListener('pjax:complete', loadWaline);

  // Astro View Transitions 完成后
  document.addEventListener('astro:page-load', loadWaline);
&lt;/script&gt;

</code></pre>
<p>你需要把其中的 serverURL 类改为真实的后端地址，你可以在其中的 css样式树上更改主题色等配置来适应自己的博客系统。</p>
<div><div><div></div><div>NOTE</div></div><div><p>这里引用了两个 unpkg 的库，你可以把它下载到本地以获得更高的性能。</p></div></div>
<p>最后在 /src/pages/posts/[…slug].astro 的layout标签中引入即可</p>
<pre><code>import Comments from '~/components/posts/base/Comments.astro';
&lt;!-- 其它导入 --&gt;
&lt;layout&gt;
&lt;!-- 其它代码 --&gt;
    &lt;!-- 评论区 --&gt;
    &lt;div class="px-4 sm:px-6 pb-6 sm:pb-8"&gt;
      &lt;Comments /&gt;
    &lt;/div&gt;
&lt;/layout&gt;
</code></pre>
<p>这样，评论系统的最基本功能就基本成型了。</p>
<h2>启用邮件通知功能</h2>
<p>你可以在后端仓库的 index.cjs 中直接修改来生效，或者创建 .env 文件来配置环境变量。不过我更推荐直接在添加 vercel 环境变量中添加，这样方便后期修改。</p>
<h4>你必须添加以下环境变量</h4>
<ol>
<li>SMTP服务器，SMTP_SERVICE（一般支持主流的邮箱 <a href="https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json%EF%BC%89" rel="noopener noreferrer" target="_blank">https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json）</a></li>
<li>SMTP用户，SMTP_USER（一般是你的邮箱地址）</li>
<li>SMTP密码，SMTP_PASS（获取方法请自行百度）</li>
<li>网站名称，SITE_NAME（站点名称，用于通知邮件中的名字显示）</li>
<li>网站地址，SITE_URL（站点链接，用于通知邮件中的超链接）</li>
<li>你执行评论时用的邮箱，AUTHOR_EMAIL（如果是自己发的评论，不会邮件通知）</li>
<li>SMTP安全连接，SMTP_SECURE（一般情况下设置为 true 就好）</li>
</ol>
<h4>我推荐设置的可选环境变量</h4>
<ol>
<li>发件人名称和发件人邮箱，SENDER_NAME 和 SENDER_EMAIL（必须同时添加，是邮件中显示的发件人名称和邮箱，邮箱的值必须和自己的邮箱地址保持完全一致）</li>
<li>邮件的样式，MAIL_TEMPLATE 和 MAIL_TEMPLATE_ADMIN（两者可以一致，用Html编写，压缩反义成单行的格式）这一步使用AI工具会快得多</li>
</ol>
<p>这里展开来讲， waline 和 Twikoo 的变量名不完全一样，如果直接把 Twikoo 的代码直接用于 waline ，大概会出现变量没被替换的情况。</p>








































<table><thead><tr><th>变量名</th><th>Waline</th><th>Twikoo</th></tr></thead><tbody><tr><td>网站名称</td><td>{{site.name}}</td><td>${SITE_NAME}</td></tr><tr><td>评论者</td><td>{{parent.nick}}</td><td>${PARENT_NICK}</td></tr><tr><td>评论内容</td><td>{{parent.comment}}</td><td>${PARENT_COMMENT}</td></tr><tr><td>回复者</td><td>{{self.nick}}</td><td>${NICK}</td></tr><tr><td>回复内容</td><td>{{self.comment}}</td><td>${COMMENT}</td></tr><tr><td>文章链接</td><td>{{site.postUrl}}</td><td>${POST_URL}</td></tr></tbody></table>
<p>你需要注意以上变量名，防止出现乱码等情况。</p>
<p>我这边再给出一个实例代码</p>
<p>MAIL_TEMPLATE</p>
<pre><code>&lt;div style="font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;max-width:640px;margin:0 auto;padding:32px 24px;background:#fff;color:#000;line-height:1.6;"&gt;&lt;div style="display:flex;align-items:center;margin-bottom:40px;"&gt;&lt;img src="https://www.refact.cc/avatar.png" alt="{{site.name}}" style="width:48px;height:48px;object-fit:contain;margin-right:12px;"&gt;&lt;span style="font-size:22px;font-weight:700;color:#000;"&gt;{{site.name}}&lt;/span&gt;&lt;/div&gt;&lt;h1 style="margin:0 0 24px 0;font-size:28px;font-weight:700;color:#000;"&gt;Hi &lt;span style="color:#e63946;"&gt;{{self.nick}}&lt;/span&gt;&lt;/h1&gt;&lt;p style="margin:0 0 32px 0;font-size:16px;color:#333;"&gt;您在 &lt;a href="{{site.url}}" style="color:#e63946;text-decoration:none;font-weight:600;"&gt;{{site.name}}&lt;/a&gt; 的评论收到了作者的回复：&lt;/p&gt;&lt;div style="margin-bottom:28px;"&gt;&lt;h2 style="margin:0 0 8px 0;font-size:18px;font-weight:700;color:#000;"&gt;您发表的评论&lt;/h2&gt;&lt;p style="margin:0;font-size:15px;color:#000;"&gt;{{self.comment|safe}}&lt;/p&gt;&lt;/div&gt;&lt;div style="margin-bottom:40px;"&gt;&lt;h2 style="margin:0 0 8px 0;font-size:18px;font-weight:700;color:#000;"&gt;作者的回复&lt;/h2&gt;&lt;p style="margin:0;font-size:15px;color:#000;"&gt;{{parent.comment|safe}}&lt;/p&gt;&lt;/div&gt;&lt;a href="{{site.postUrl}}" style="display:inline-block;padding:14px 28px;background:#000;color:#fff;text-decoration:none;font-weight:700;font-size:15px;"&gt;查看详情&lt;/a&gt;&lt;hr style="border:none;border-top:1px solid #000;margin:48px 0 16px 0;" /&gt;&lt;p style="font-size:12px;color:#666;margin:0;"&gt;此邮件由系统发送，请勿回复。&lt;/p&gt;&lt;/div&gt;
</code></pre>
<p>MAIL_TEMPLATE_ADMIN</p>
<pre><code>&lt;div style="font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;max-width:640px;margin:0 auto;padding:32px 24px;background:#fff;color:#000;line-height:1.6;"&gt;&lt;div style="display:flex;align-items:center;margin-bottom:40px;"&gt;&lt;img src="https://www.refact.cc/avatar.png" alt="{{site.name}}" style="width:48px;height:48px;object-fit:contain;margin-right:12px;"&gt;&lt;span style="font-size:22px;font-weight:700;color:#000;"&gt;{{site.name}}&lt;/span&gt;&lt;/div&gt;&lt;h1 style="margin:0 0 24px 0;font-size:28px;font-weight:700;color:#000;"&gt;Hi &lt;span style="color:#e63946;"&gt;{{parent.nick}}&lt;/span&gt;&lt;/h1&gt;&lt;p style="margin:0 0 32px 0;font-size:16px;color:#333;"&gt;您的 &lt;a href="{{site.url}}" style="color:#e63946;text-decoration:none;font-weight:600;"&gt;{{site.name}}&lt;/a&gt; 网站收到一条新评论：&lt;/p&gt;&lt;div style="margin-bottom:40px;"&gt;&lt;h2 style="margin:0 0 8px 0;font-size:18px;font-weight:700;color:#000;"&gt;您收到的评论&lt;/h2&gt;&lt;p style="margin:0;font-size:15px;color:#000;"&gt;{{self.comment|safe}}&lt;/p&gt;&lt;/div&gt;&lt;a href="{{site.postUrl}}" style="display:inline-block;padding:14px 28px;background:#000;color:#fff;text-decoration:none;font-weight:700;font-size:15px;"&gt;查看并回复&lt;/a&gt;&lt;hr style="border:none;border-top:1px solid #000;margin:48px 0 16px 0;" /&gt;&lt;p style="font-size:12px;color:#666;margin:0;"&gt;此邮件由系统发送，请勿回复。&lt;/p&gt;&lt;/div&gt;
</code></pre>
<p>这个模版的效果如下，你必须更改其中的logo链接，或者是直接删除图标。</p>
<img src="https://img.refact.cc/comment.png" alt="评论系统实例图" />
<h2>结尾</h2>
<p>这样就完成了一个较为专业的评论系统的部署，还有很多细节，本文并未列出。你可以访问官方文档来查询更多细节。</p>
<h3>参考文献</h3>
<p><a href="https://waline.js.org/guide/get-started/" rel="noopener noreferrer" target="_blank">Waline 官方文档</a></p>
<p><a href="https://oragekk.me/blog/waline-mail.html" rel="noopener noreferrer" target="_blank">oragekk的博客</a></p>
<p><strong>共 1859 词</strong></p>
<p>2025年9月26日 在macOS Tahoe 26.0 ｜ Typora 编辑</p>
<p>2025年11月22日 第一次修改</p>]]></content>
    <category term="评论系统" />
    <category term="部署" />
    <category term="前端" />
  </entry>
</feed>