<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://blag.dsstudio.tech/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
    <title>豆沙工作室</title>
    <link>https://blag.dsstudio.tech/</link>
    <atom:link href="https://blag.dsstudio.tech/feed.xml" rel="self" type="application/rss+xml" />
    <description>一人多役的艺术</description>
    <language>zh</language>
    <follow_challenge>
        <feedId>107427218643910656</feedId>
        <userId>LhUoD4sIRBhBBh14M3JtnBuu2peQk7jy</userId>
    </follow_challenge>
        
<item>
<title>关于 `:visited` 伪类</title>
<link>/archives/clean-slate-on-visited.html</link>
<description><![CDATA[<html><head></head><body><p>最近读到了 <a href="https://kevquirk.com/do-you-use-visited-on-links" class="u-like-of" rel="like-of">Kev Quirk 关于 <code class="inline">:visited</code> 样式的文章</a>，我认为有必要跟进实现。不过在实现的过程中，我还是注意到了一些非常有趣的问题。</p>
<p></p><p></p>
<p>修改链接颜色很简单，直接指定 <code class="inline">a:visited</code> 的 <code class="inline">color</code> 样式就可以了。不过，我发现如果简单地套用浏览器默认样式会带来一个很尴尬的问题——</p>
<h2>蓝紫组合对色弱人士不友好</h2>
<p>标准的浏览器行为是：新链接使用蓝色表示；访问过的链接用紫色表示，也就是：</p>
<section aria-label="样块视图"><div class="tear-top"></div><div class="example"><div style="display: flex; flex-flow: row; flex-wrap: wrap; gap: 1rem;">
<div style="width: 6rem; height: 6rem; background-color: blue; color: white; text-align: center;">
  <p>蓝色样块</p>
</div>
<div style="width: 6rem; height: 6rem; background-color: purple; color: white; text-align: center">
  <p>紫色样块</p>
</div>
</div>
</div><div class="tear-bottom"></div></section><p>但如果你是红绿色弱或色盲的话，你实际看到的样块颜色会类似于这样：</p>
<section aria-label="模拟红绿色盲的视图"><div class="tear-top"></div><div class="example"><div style="display: flex; flex-flow: row; flex-wrap: wrap; gap: 1rem;">
<div style="width: 6rem; height: 6rem; background-color: blue; color: white; text-align: center;">
  <p>蓝色样块</p>
</div>
<div style="width: 6rem; height: 6rem; background-color: #001b86; color: white; text-align: center">
  <p>紫色样块</p>
</div>
</div>
</div><div class="tear-bottom"></div></section><p>这两个样块之间的颜色差异不甚明显。</p>
<p>一般这种问题的通行解法是：不要让信息只通过一个渠道传递。即使是纯视觉的元素，也可以通过明度和色度两个通道来传递。具体的，我们可以考虑给这两种不同的链接安排不同的下划线样式，或者在链接尾部追加一个小图标，或者……</p>
<h2>但是 <code class="inline">:visited</code> 是特殊伪类</h2>
<p>因为它携带用户的隐私信息，所以无论是指定不同的下划线样式，还是 <code class="inline">::after</code>, 甚至 <code class="inline">&gt;</code>, <code class="inline">+</code>, <code class="inline">:has()</code> 等等组合子都会被浏览器静默地忽略<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。CSS 唯一能够修改的，只有颜色，甚至连 Alpha 通道都无法修改。</p>
<p>借助 JavaScript 的力量也是不行的。<code class="inline">getComputedStyle(.)</code> 总是会返回链接未被访问过的样式；<code class="inline">document.querySelector(.)</code> 也不会匹配 <code class="inline">:visited</code> 选择子<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>。毕竟，开放这项能力相当于允许 JavaScript 脚本无感地探测用户是否访问过给定的网站，这个能力可真是太强大了。</p>
<p>在 2022 年，我撞到了 <a href="https://varun.ch/posts/history">Varun Biniwale 的伪装成验证码的历史嗅探实现</a>。不过比较遗憾的是（或者说幸好），这个演示里所使用的技巧并不能绕开浏览器针对于 <code class="inline">:visited</code> 选择器的限制；也无法用于给已访问的链接添加更多新的样式。</p>
<p>那么，这意味着一件事：想要改善可访问性，我们就必须跳脱出浏览器的默认颜色表。</p>
<h2>挑选更友好的色彩搭配</h2>
<p>好在「什么颜色搭配是色盲友好的」这个问题已经有现成答案了。参考 NCEAS 给出的色彩搭配<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>和 Dawn Bringer 32 色盘表，我最终决定了使用如下颜色值：</p>
<pre><code class="hljs language-css"><span class="hljs-selector-tag">a</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-built_in">light-dark</span>(<span class="hljs-number">#306082</span>, <span class="hljs-number">#cbdbfc</span>);
}

<span class="hljs-selector-tag">a</span><span class="hljs-selector-pseudo">:visited</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-built_in">light-dark</span>(<span class="hljs-number">#45283c</span>, <span class="hljs-number">#eec39a</span>);
}
</code></pre>
<p>虽然看起来有点怪（尤其是在暗色主题下），但这样的颜色搭配可以照顾到红绿蓝三种可能的情况。可惜的是受限于文字对比度要求，我实在是没法照顾全色盲/灰度显示器上的阅读体验了。</p>
<hr><aside aria-labelledby="footnote-and-references"><h2 id="footnote-and-references">参考与注释</h2><ol><li id="fn1" class="footnote-item"><p>Mozilla. <code class="inline">:visited</code> CSS pseudo-class[F/OL]. MDN. (2026-09-10)[2026-09-26]. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:visited">https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:visited</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Mozilla. Privacy and the :visited selector[F/OL]. MDN. (2025-11-07)[2026-09-26]. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Privacy_and_:visited">https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Privacy_and_:visited</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Phillips A. Colorblind Safe Color Schemes[EB/OL]. NCEAS Science Communication Resource Center. (2022-06-29)[2026-09-28]. <a href="https://www.nceas.ucsb.edu/sites/default/files/2022-06/Colorblind%20Safe%20Color%20Schemes.pdf">https://www.nceas.ucsb.edu/sites/default/files/2022-06/Colorblind Safe Color Schemes.pdf</a>. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol></aside></body></html>]]></description>
<pubDate>Mon, 28 Sep 2026 11:00:28 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>Clean Slate: 一个 11ty 站点主题的实现</title>
<link>/archives/clean-slate.html</link>
<description><![CDATA[<html><head></head><body><p>稳定的接口是对接外部系统的基石，但是不当的假定不应该升格成为接口，无论这种假定看起来有多理所应当。</p>
<p></p><p></p>
<p>最近随着<a href="/special/tags/accessibility/">无障碍访问改善工作</a>的进行，我发现这个主题里面包含的问题还是挺多的。其中有一些可能是因为当时<a href="/archives/202501.html">转 11ty</a> 时没有完整地实现整个页面的行为，比如最近才修好的暗色主题没有正确应用情况；另一些则是主题本身就带着的实现缺陷，比如<a href="/archives/11ty-a11y-fixing-the-image-viewer.html">图片灯箱组件没有正确的焦点</a>。</p>
<p>我注意到这个主题有对语义化 HTML 的理解和规范不一致的情况。一开始我计划通过 <code class="inline">custom.css</code> 结合尽可能小的模板修改去一点点修掉「不符合认知」的部分，但是修着修着就发现补丁是打不完的，而且经常会按下葫芦浮起瓢。由于原先主题的 CSS 对于 HTML 的嵌套顺序有非常特殊的要求，修改 HTML 元素意味着大批量地修改 CSS, 而且通常修改完成之后总是看起来和原始设计差一点。要命的是，差这一点所带来的「不匀称」的感觉非常明显。</p>
<p>在实现 <a href="/special/tags/indieweb/">IndieWeb 相关的各种小工具</a>的时候，我发现要对模板做的修改就更多了：<a href="/archives/pager-webmention.html">引入 Webmention</a> 并不仅仅是引入 Webmention 本身，它也意味着引入一堆 IndieWeb 给出的其他怪东西，比如你需要正确地实现 <a href="https://microformats.org/wiki/microformats2">microformats2</a> 才能「正确」地给其他站点发送 Webmention.</p>
<p>事已至此，我想或许我也可以行使一下我的主观能动性，再来一场酣畅淋漓的狂野 Web 开发！</p>
<h2>不破不立</h2>
<p>首先，移除所有的样式和脚本，使得站点退化到只有纯 HTML 结构的状态。去除了样式和交互之后，就更容易看出哪些 HTML 构造是有问题的，以便后续进行重构。</p>
<h2>去伪存真</h2>
<p>先修复文章列表页面的 HTML 语义问题——原有的列表页依赖于 <code class="inline">&lt;article&gt;</code> 和 <code class="inline">&lt;main&gt;</code> 元素的组合，使得列表的每个条目都包含了 <code class="inline">&lt;main&gt;</code> 元素。这个在 HTML 语义上实际是不允许的<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。自然，这些问题组合需要被移除重写。</p>
<p>同样的，冗余的 <code class="inline">&lt;div&gt;</code> 和 <code class="inline">&lt;section&gt;</code> 嵌套也可以一并删除。大部分的 <code class="inline">class</code> 属性也可以尽数移除。我尽可能地将原来层层嵌套的文本上移又上移——得益于现代浏览器的特性，我已经可以不再用 <code class="inline">&lt;div&gt;</code> 套 <code class="inline">&lt;div&gt;</code> 的方式来做排版了；同样，我也不需要再用一个单独的控件来切换亮色和暗色主题了：<code class="inline">@media (prefers-color-scheme)</code> 在绝大多数浏览器上都已经有了很好的支持<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>，所以这个站点可以直接随着你的浏览器全局设置甚至系统的全局设置自动调整色盘。</p>
<p>去除这些冗余的元素，并换用带有语义的 HTML 元素，很大程度上减少了无障碍访问需要额外做的工作（比如给各种 <code class="inline">&lt;div&gt;</code> 或者 <code class="inline">&lt;section&gt;</code> 指定 <code class="inline">role</code> 属性，或者给一些纯粹的视觉装饰元素指定 <code class="inline">aria-hidden</code> 属性）。同时，由于去掉了亮暗切换控件，Tab 键顺序也不再需要手动调整了。</p>
<p>每个页面都由三个主要部分组成：头、身、尾，对应 <code class="inline">&lt;header&gt;</code>, <code class="inline">&lt;main&gt;</code>, <code class="inline">&lt;footer&gt;</code>. 头部主要是页面大标题、导航栏，以及文档的必要元信息；身体自然是文章列表或者文章正文；尾巴则是底部导航、版权信息和合规性小字，组织起来大概就是：</p>
<pre><code class="hljs language-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- 从略 --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>大标题<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#main"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>&gt;</span>跳到内容<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">a</span>&gt;</span>顶部导航<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Yours Truly<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">time</span>&gt;</span>2026-12-31<span class="hljs-tag">&lt;/<span class="hljs-name">time</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">article</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"main"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>正文<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">article</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">footer</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>底部导航<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-symbol">&amp;copy;</span> 2026<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-comment">&lt;!-- 实际页面的实现可能和上述结构有所差异 --&gt;</span>
</code></pre>
<p>现在，我们有了一个良好标记的文档，接下来就是应用一些样式了。</p>
<p>因为我需要手写模板和 CSS 样式，所以核心框架就是：在能够满足「匀称」的前提下，尽可能少做一些 <code class="inline">&lt;div&gt;</code> 套 <code class="inline">&lt;div&gt;</code> 的动作、少引入各种样式，并且尽可能少写几行 CSS. 同时，生成的页面结果需要至少能过 <a href="https://github.com/validator/validator">Nu HTML Checker</a> 而不报错。</p>
<h2>大道至简</h2>
<p>从整体的观感上，我想模拟纸张和卡片的质感，但是我实在是不想手写整套老的 Material Design 的那堆效果，毕竟我想要的是很简单的东西：简单的白纸、简单的文字、干净而稳定的呈现。它不会有复杂的动画效果，也不考虑任何元素间的交互。换句话说，它应当是一本纸质笔记本的数字化呈现。</p>
<p>自然的，顶层容器就是笔记本上的纸张啦。我琢磨了一小会「如何让背景看起来像纸」，最终觉得一比一地还原纸张的样貌的工作量有点超预算：因为无论是通过加载一张图片作为背景拼贴，还是通过脚本动态地计算纸张的材质，都要花费不少功夫（和网络带宽）。虽然尝试通过代码绘制一个带有反射率各向异性的材质是一个不错的计算机图形学练习，但我实在是写不动了。</p>
<p>不过，我们并不需要真的去一比一地模拟纸张。我们只需要「提示」这是一个「有层级」的部件就足够了。Web 给我们用于提示层级的最直接的手段就是边框投阴影（笑）。</p>
<section aria-label="样块应用不同阴影的样例"><div class="tear-top"></div><div class="example"><div style="display: flex; flex-flow: row; flex-wrap: wrap; gap: 32px;">
  <div style="width: 8rem; height: 8rem; border: 1px solid light-dark(rgb(155, 173, 183), rgb(48, 96, 130)); text-align: center;">
    <p>无阴影的样块</p>
  </div>
  <div style="width: 8rem; height: 8rem; border: 1px solid light-dark(rgb(155, 173, 183), rgb(48, 96, 130)); text-align: center; box-shadow: 0 2px 4px
    light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.8));">
    <p>有阴影的样块</p>
  </div>
  <div style="width: 8rem; height: 8rem; border: 1px solid light-dark(rgb(155, 173, 183), rgb(48, 96, 130)); text-align: center; box-shadow: 0 4px 8px
    light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.8));">
    <p>重阴影的样块</p>
  </div>
</div>
</div><div class="tear-bottom"></div></section><p>这样看起来就足够了。同时，为了体现出纸张硬质的质感，我选择不给容器加圆角。不过内联代码的边框作为文字的一部分，是可以更圆润的。</p>
<p>虽然名为网页的稿纸本身并没有打格子，我还是决定正文的最大宽度是四十列汉字，对应等宽西文字体的 80 列宽。我一直觉得 80 列宽是一种非常有趣的巧合。尽管显示器的宽度自 IBM 终端机时代已经拓宽了不少，但是人眼一行能扫描的宽度却并没有随之增加。在 12pt 的字号下，80 列等宽字符几乎刚好是最大阅读宽度。</p>
<h2>以人为本</h2>
<p>无障碍访问自然是此次设计的重点。使用语义化的 HTML 配合适当的 <code class="inline">aria-label</code> 和 <code class="inline">aria-labelledby</code> 可以给屏幕阅读器提供各个区域的类型（比如这个部分是正文还是导航栏），以便用户快速地在区域间跳转。当 VoiceOver 焦点移动到一个看起来没有标记的框框上之后却能够正确地说出「文章标签，补充；你当前在网页内容的补充上」的时候，我确实有一股莫名的欣慰感上头。</p>
<p>对于评论区这种会有动态内容加载的区域，可以指定 <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live"><code class="inline">aria-live</code></a> 属性。这样，当这个元素内部有 DOM 变更时，屏幕阅读器就会按要求向用户播报。</p>
<p>在挑选站点颜色的时候，我按照习惯选择了 <a href="https://lospec.com/palette-list/dawnbringer-32">DawnBringer 32 色盘</a>，并遵照 WebAIM 的对比度要求调整了各个文字控件的色彩搭配。当然，部分特殊结构的色彩选择可能仍然会出现对比度不足的情况（比如对话窗格的系统文字），这个只能先无奈妥协了，毕竟它需要遵循原始文稿定下的设计。</p>
<p>同时，也需要强调一下：深色主题也是无障碍访问的一部分。尽管<a href="/archives/my-ideas-against-dark-mode.html">我个人更偏好使用浅色主题</a>，深色主题也是不能落下的。同样的，深色主题也要根据 WebAIM 的要求调整文字对比度，以及调整其他视觉元素的颜色，避免出现在深色模式下用浅色控件的情况。</p>
<p>因为这回我对于页面样式有了全权控制，所以代码块现在也增加了颜色自适应能力——终于不用在浅色背景上糊一个深色代码块了！</p>
<p>无障碍访问自然是需要持久地改善的，随着 WAVE 工具报告 0 错误之后，我终于算是挣得了「改善」而不是「修复」的资格了。</p>
<p>有了更简洁的 HTML 结构，之后我们引入 IndieWeb 所需要的完整的标记架构就会更容易一些。目前这套主题已经开始实验性地包括一些基本的 microformats2 的语义标记，至少 Firefox 的阅读器视图中已经可以识别诸如作者名之类的信息了。</p>
<p>后续的工作自然包括通过工具验证页面的结构是否正常以及提供更多的必要信息，以完整地支持 Webmention 所需要提供的内容。</p>
<h2>尾声</h2>
<p>虽然站点大小并不是工作的重点，不过甩掉了 jQuery 这个大头之后，站点总共需要加载的样式和脚本下降到了 85kB+. 我所编写的样式和脚本由于没有过最小化，每个也有 5kB~12kB, 不过这个通过 Gzip 压缩后应该并不构成问题。</p>
<p>当然，目前这套结构还是有一些缺憾的，比如包含内联代码的行高和普通文字的行高不一样，所以一些情况下看起来会有些奇怪；以及一些中文标点的排版仍然有改进的空间，比如冒号和引号、括号和逗号等之间应考虑共享空白空间（各自占半个字）。或许这套主题应该加一些交互的小动效，现在看起来实在是有些呆板了。</p>
<p>而且，毕竟是重构，这套主题里应该还会有一些我没有注意到的小细节没能正确地实现。尽管我在实现这套主题的时候翻看了之前已经发布的全部文章并测试了评论区的功能，但我并不会惊讶某天我又会在查阅历史文章时发现一个看起来十分别扭的元素。只能希望未来的我不会痛骂今天的我的小小疏忽吧 <code class="inline">\blep{}</code>。</p>
<p><a href="https://biji.io/">Tokin 兄</a>，谢谢你的 <a href="https://github.com/Tokinx/Adams">Adams 主题</a>，我真的很喜欢！感谢六年的陪伴，再见。</p>
<h2>后日谈：你知道你的站点在不同的浏览器下看起来是不一样的么？</h2>
<p>我知道，因为我在设计整套主题的时候都在一个<a href="https://support.mozilla.org/en-US/kb/resist-fingerprinting">开了浏览器指纹对抗</a>的 Firefox 上进行，等我调整完了之后出于纯粹的好奇，用 Safari 打开了页面，发现页面渲染的结果略微略微地有那么点不同。好在看起来还行，不至于到不匀称的地步。</p>
<p>不过我认为这并不是一个大问题，因为这个现象来自一个刻意的选择：</p>
<p>因为我在写 CSS 的时候，选择了使用 <code class="inline">1rem</code> 而不是 <code class="inline">16px</code> 作为基本尺寸，所以如果浏览器选中的字体有差别的话，整个文档渲染出来的结果就是不一样的。</p>
<p>这其实也是无障碍设计的一部分。如果有人需要默认字号更大一些的话，那么这套框架也不会产生行压行的尴尬情景；如果有人喜欢正文使用他们自己指定的字体的话（比如<a href="https://opendyslexic.org/">辅助失读症人士阅读的字体</a>），这套排版规则也会跟着自动调整。</p>
<hr><aside aria-labelledby="footnote-and-references"><h2 id="footnote-and-references">参考与注释</h2><ol><li id="fn1" class="footnote-item"><p>Mozilla. HTML main element[F/OL]. MDN. (2026-09-10)[2026-09-16]. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/main#usage_notes">https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/main#usage_notes</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Fyrd. prefers-color-scheme media query[FD/OL]. CanIUse. (2026)[2026-09-16]. <a href="https://caniuse.com/prefers-color-scheme">https://caniuse.com/prefers-color-scheme</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol></aside></body></html>]]></description>
<pubDate>Sat, 19 Sep 2026 14:41:28 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>[11ty] 无障碍设计：修复图片灯箱</title>
<link>/archives/11ty-a11y-fixing-the-image-viewer.html</link>
<description><![CDATA[<html><head></head><body><p>或者说，正确地实现一个模态对话框。</p>
<p></p><p></p>
<p>虽然标题里说的是 11ty, 其实更多的是在修 <a href="https://github.com/Tokinx/Adams">Adams 主题</a>。不知不觉间这款主题现在已经快到要上学的年纪了。</p>
<p>Adams 主题在点击图片的时候会显示大图，通过 <a href="https://tokinx.github.io/ViewImage/">ViewImage 库</a>实现。这个库本身功能上没什么问题，但是它会动态地插入灯箱 <code class="inline">&lt;div&gt;</code> 元素，而这会对屏幕阅读器带来不小的挑战：灯箱打开之后，它是没有任何屏幕阅读器可见的输入焦点的，因为它的所有看起来像是按钮的东西都是一个带着点击事件且没有 <code class="inline">tabindex</code> 的 <code class="inline">&lt;div&gt;</code>。这就意味着如果你尝试用屏幕阅读器去关闭灯箱，那么不好意思，做不到——屏幕阅读器甚至不知道灯箱这个东西被打开了！它的焦点还停留在图片上面。</p>
<p>虽然 ViewImage 库实现了 Esc 键的输入捕获来通过键盘关闭灯箱，但我总觉得这么做有点不是滋味。</p>
<h2>「幽灵窗口」</h2>
<p>在 <a href="https://www.applevis.com/blog/accessibility-issues-must-not-be-named">Accessibility Issues That Must Not Be Named</a> 中，PaulMartz 指出这种无法被屏幕阅读器访问的窗口十分常见。更头疼的是：模态对话框内可能会包含真的需要用户交互的内容，而 VoiceOver 的高亮框就是进入不到这个对话框里。</p>
<p>它「看」起来像是一个模态对话框，「点」起来像是一个模态对话框，但是它并不会把自己标识为一个模态对话框，也不会提供对应的 ARIA 属性来告诉屏幕阅读器这是一个模态对话框。对于屏幕阅读器来说，它和一个没有任何交互的 <code class="inline">&lt;div&gt;</code> 幽灵没有任何区别。</p>
<p>即使对话框内部有可被识别的元素，许多对话框的实现也需要用户移动到页面最底部才能将扫描焦点移动到对话框内，因为对话框本身是被动态插入到 <code class="inline">&lt;body&gt;</code> 元素中，作为最后一个子元素出现的。</p>
<p>所以，如果要正确实现一个模态对话框，我们至少需要满足以下需求：</p>
<ul>
<li>屏幕阅读器可以感知到模态框被打开</li>
<li>屏幕阅读器可以在模态框内探索其中的元素，尤其是关闭按钮必须可以被选中（笑）</li>
<li>输入焦点需要跟随到模态框内，这样就不需要按一百多次 Tab 才能选中关闭按钮了——尤其是你的键盘焦点是在页面正中心所以 Shift+Tab 和 Tab 都得按几十次的时候</li>
</ul>
<p>考虑到 ViewImage 库是 IE 时代的产物，它必然会需要照顾到各种老浏览器的怪行为。但现在我总算是摆脱了 IE 的桎梏，有了现代浏览器的功能加持，能否改善图片灯箱的可访问性呢？</p>
<h2>现有技术</h2>
<p>在着手写自己的实现之前，我先简单搜索了一下现有的灯箱库：</p>
<ul>
<li><a href="https://lokeshdhakar.com/projects/lightbox2/">Lightbox2</a>: 虽然这个无疑是最优实现，但是它依赖于我正在计划移除的 jQuery, 所以很遗憾</li>
<li><a href="https://litebox.js.org/">LiteBox</a>: 同样有无障碍访问问题</li>
<li><a href="https://fslightbox.com/javascript">fslightbox.js</a>: 虽然确实正确使用了 <code class="inline">&lt;button&gt;</code> 作为灯箱控件，但是灯箱控件需要按无数次 Tab 才能得到焦点</li>
<li><a href="https://noelboss.github.io/featherlight/">Featherlight.js</a>: 这玩意一下子干碎了我的 <a href="https://wave.webaim.org/extension/">WAVE 插件</a>，含金量太高了</li>
</ul>
<p>那么，自己动手，丰衣足食！</p>
<h2><code class="inline">&lt;dialog&gt;</code> 元素</h2>
<p>HTML 5 标准中添加了 <code class="inline">&lt;dialog&gt;</code> 元素。这个元素天然地可以作为模态对话框——它有完整的可访问性语义、焦点、自动抬升的 <code class="inline">z-index</code>, 用起来真是再好不过了！而且不需要担心兼容性问题：<a href="https://caniuse.com/dialog">三大浏览器内核对这个元素都有足够的支持</a>。</p>
<h2>HTML 5 的 <code class="inline">data-</code> 属性前缀</h2>
<p>为了保持 HTML 5 标准合规，自定义的属性应该以 <code class="inline">data-</code> 开头。这个修起来很容易，重新指定一下属性名可以了。</p>
<h2>我真的需要一个图片灯箱么？</h2>
<p>思来想去，目前的用例中，图片灯箱似乎没有什么特别明显的作用：如果用户需要看大图的话，他们完全可以在新标签页中打开图片，这样他们甚至可以享受浏览器自带的缩放和保存功能。我的博客的图片大多托管在图床上，没有转码也没有压缩，所以也不需要灯箱来加载真·高清大图出来——大多数灯箱插件甚至也压根没有点开之后加载另一张图片的方法。</p>
<p>一个可能的用例是允许用户直接在页面中缩放一些比例比较奇特的图片，比如放大窄长的截图。不过这个功能在原版的 ViewImage 库里面貌似也没有实现——这件事还是交给浏览器缩放自己来吧！</p>
<p>另一个可能的方案是：在灯箱中提供关于图片的更多信息，比如拍摄位置、镜头参数等图片元信息、版权信息，以及展示该图片的描述文字等等。不过这么搞，可能带来的坑会更多——是否需要专门的页面来展示这些信息？这些信息的无障碍访问又要怎样处理？</p>
<p>还是先不搞这些花里胡哨的东西了。它现在可以满足需求，这是最重要的。</p>
</body></html>]]></description>
<pubDate>Sat, 19 Sep 2026 12:37:07 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>不想（二）十七岁</title>
<link>/archives/202603.html</link>
<description><![CDATA[<html><head></head><body><p>终于又到了这一天。我本来以为它会迟点来，但是似乎守时是它无上的美德。</p>
<p></p><p></p>
<p>我本来认为，这一天应该会有一些事情发生。是不是好事我并不清楚，但应该有一些标志性的事情发生。不过现实地来讲，有很多想要办成的事情都远非能一蹴而就的：它们要么需要大量的精力投入；要么受制于许多现实因素而无法在很短的时间内完成；要么过于天马行空以至于完全没有着力点。</p>
<p>这种愿景的落空使得我不得不再次审视我当前的工作状态。自从冲击以来，我发现我的工作状态就没怎么好过。应对这种改变在不知不觉中抽去了我的一大半精力，而正经上班的各种事情则更是让人烦扰。我发觉自己已经有很长一段时间没有那种因创作而喜悦的感觉了。</p>
<p>不想二十七岁，但时间不等人；但是即使意识到时间的匆匆，似乎对「做点什么」这个概念也很难提起兴趣。之前是想着「<a href="/archives/3213.html">再做点什么吧</a>」，现在的我发现自己还是没能成功跳出这个怪圈。虽然确实能做的事情慢慢变多了起来——我仍然在尝试为各种新的领域增加一些实验性支持，其中不乏一些并不属于计算机大类的领域；但人实在是没法把自己摊得太薄。</p>
<p>如果接受社会的时间安排，二十七岁，总该是成家了吧？但是不必多说，这件事到现在自然是一点没有着落。表面上，我还可以继续用工作的高饱和来提示此方面所需要投入的精力远比我目前可以分配的要多得多，借此来搪塞所有来自家里的问题；内里则越来越觉得或许这辈子孤独终老或许不是一种选择，而是一种必然。</p>
<p>但哪怕你给自己挂一面橙白蓝/绿白紫，对于绝大多数人来说，那也只不过是一种借口。这里的绝大多数甚至比我愿意承认的比例还要大一些：这方面的小众可一点也不酷。再说了，难道你到现在还没遇到那个可以与你分食蒜蓉面包的好朋友么？</p>
<p>或者可以选择完全拒绝社会的时间安排，但我却又不得不再次看向这守时的刈麦人。或许我可以无视「什么年龄该干什么事情」的说法，但我的身体却也一天天地变得不堪用起来。「适龄」这个词正变得越来越刺眼，或许这也是对于最终结论的不安的体现。</p>
<p>不想二十七岁，其实也不想十七岁：不想回首发现自己虚度了年华，也不想抬头望见自己未能走遍天涯。试着去做更多的事情吧！希望下一次，你也能准时地让你的愿望如期到达。</p>
</body></html>]]></description>
<pubDate>Tue, 08 Sep 2026 14:44:58 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>小点阵字</title>
<link>/archives/smol-text.html</link>
<description><![CDATA[<html><head></head><body><p>在螺蛳壳里做道场，真的可行么？</p>
<p></p><p></p>
<p>最近一些事务要求我能够用尽可能少的像素数量绘制一些文字，具体的，我需要保证每个需要被用到的汉字可以被装进 8x8 像素的点阵中。</p>
<p>因故关注过<a href="https://3type.cn">三言</a>的同志们可能立刻会想到<a href="https://atelier-anchor.com/typefaces/dinkie-bitmap">丁卯点阵体</a>，它的 7px 尺寸版本正好满足这个要求。不过遗憾的是，现在丁卯点阵体的个人授权已经无法购买了。我实在是不好意思从<a href="https://pan.icu/">内核恐慌</a>的主页上扣字形下来（以及他们那边用的是 9px 的版本），所以只有一个选择：</p>
<p>自己动手，丰衣足食！</p>
<p>虽然说是 8x8 的点阵，但实际上字面只有 7x7 像素可用：要留出 1 像素的白边以便密排。好在我（暂时）并不需要绘制全套的字形，所以就尽可能地即兴发挥——</p>
<figure>
<img alt="警告标牌，以一个黑边黄色背景正三角形为背景，由一个灯泡和叠加在灯泡上的闪电组成的示意图。底部文字为「当心认知污染」" src="/assets/image/badges/cognitohazard-captioned.png" width="128" style="image-rendering: -moz-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; border: 1px solid black;" data-view-image-ignore="">
<figcaption>当心认知污染</figcaption>
</figure>
<p>是的，有点老套了。但是工科审美差不多就是这个样子嘛，要啥自行车！</p>
<p>更「正确」的说法应当是「当心认知危害」。由于「害」字我实在是没法在 7x7 的点阵中画出来，所以只能退而求其次，替换更好绘制的同等说法了。这种对于文案的妥协后续还会继续出现。</p>
<p>需要注意到：尽管这些字在丁卯点阵体演示版本中都有提供，我还是选择尽可能不参考既有方案，而是自己先尝试绘制。没错，它会看起来很不匀称或者干脆很丑，但是毕竟 7x7 能给出的排列组合并不多；而且似乎演示版本中的一些字形看起来……更不协调？我希望实现的效果是每个字尽可能等宽，毕竟差一个像素就差了大约 14.28%。以「当」字为例，丁卯的方案绘制出来是 6 像素宽，如果放在其他 7 像素宽的文字之间，就会留下一个尴尬的白条。所以我就需要自行绘制 7 像素宽的字形作为替代。况且，我实在是不想因为个人的一点不成形的爱好染上诸如字体版权之类的麻烦事情。</p>
<p>容易注意到，为了能够将一些字塞到 7x7 的框架中，必须牺牲一些笔画的准确性。最明显的就是「染」字，它的三点水和「九」部件就无法真正完整地绘制出来。在黑白两色的框架约束下，也无法借助 RGB 特征绘制「半个像素」这样的东西，所以黏连和合并就成了唯一的方案。</p>
<p>有了警告标牌，很难不趁热打铁做一个强制要求标牌——</p>
<figure>
<img alt="强制要求标牌，以一个蓝色圆形为背景，由墙壁的图形和叠加在上面的火焰图形组成的示意图。底部文字为「必须启用认知滤网」" src="/assets/image/badges/cognito-filter.png" width="128" style="image-rendering: -moz-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; border: 1px solid black;" data-view-image-ignore="">
<figcaption>必须启用认知滤网</figcaption>
</figure>
<p>「必须使用」会比「必须启用」更自然。但是我花了大概一个多小时琢磨怎么把「使」字画出来，最终决定放弃。7 个像素无论怎么摆放，都很难表达出「吏」的结构。</p>
<p>「滤网」能够绘制出来也是纯属运气好：「滤」的「七」和「心」刚好可以被挤压到一起形成一个看似可行的结构；而「网」的「乂」之间也可以刚好共享中心的像素。</p>
<p>不过，即使再在文案上做妥协，有些时候还是逃不了课的——</p>
<figure>
<img alt="安全状况标牌，以一个绿色正方形为背景，由一个按钮和正在按下按钮的手组成的示意图。底部文字为「如有危况按此急停」" src="/assets/image/badges/hold-reset-normal.png" width="128" style="image-rendering: -moz-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; border: 1px solid black;" data-view-image-ignore="">
<figcaption>如有危况按此急停</figcaption>
</figure>
<p>「停」这个字上来就超过了我的设计能力，所以磨了半天只能祭出二简字这个大轮椅硬抗。「急」字也是相当抽象，但好在还是硬糊出来了。</p>
<p>这个标牌的原本有另一个图样设计：</p>
<figure>
<img alt="安全状况标牌，以一个绿色正方形为背景，由一个按钮和正在按下按钮的手组成的示意图。底部文字为「如有危况按此急停」" src="/assets/image/badges/hold-reset.png" width="128" style="image-rendering: -moz-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; border: 1px solid black;" data-view-image-ignore="">
<figcaption>如有危况按此急停（替代版本）</figcaption>
</figure>
<p>不过我最终放弃了提交这个版本：如果你看到了这个标牌长这样，并且理解为什么它会是这样，那么你已经就已经受到污染啦！✿✿ヽ(°▽°)ノ✿ Welcome to the club!</p>
<p>我实在是没有什么动力再去画一个禁止标牌了，本月的文体活动量已经达标了，这些事情之后安排到 9 月再说吧。</p>
<p></p><p></p>
<p>到这里不禁还是感叹，给英语设计一个 8x6 像素的等宽字体相对轻松许多，你甚至可以顺手加上<a href="https://en.wikipedia.org/wiki/Box-drawing_characters">用于画框框的各种字符</a>。而想要驯服汉字屈尊于 8x8 像素之间，果真还需下更多功夫！</p>
</body></html>]]></description>
<pubDate>Wed, 26 Aug 2026 16:30:48 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>Get an Oscilloscope</title>
<link>/archives/get-an-oscilloscope.html</link>
<description><![CDATA[<html><head></head><body><p>年轻人的第一台示波器。</p>
<p></p><p></p>
<p>对于搞硬件的人来说，示波器几乎是必不可少的工具。但是由于囊中羞涩，很长一段时间我只能通过那个 50 块的 Saleae Logic 山寨逻辑分析仪用 12M 的采样率勉强看一下自己画出来的小东西到底是有什么问题。（我知道这玩意最高是 24M, 但因为毕竟是山寨的，24M 下工作非常不稳定，要持续长时间抓波形只能跑 12M.）</p>
<p>倒没有说 50 块的逻辑分析仪有啥不好——学生时代抢不到学校又新又好的是德机器的时候，只能退而求其次用隔壁的 50MHz 惠普模拟示波器；而整个实训楼有示波器的台子都被占满的时候，这个只需要一星期饭钱的小玩意真是能救人于水火。</p>
<p>但一到模拟电路部分，逻辑分析仪就彻底没法救了，无论怎样都需要一台像样的示波器才行。而且从学校毕业之后，想再在自己空闲的时候蹭一台示波器用就更不可能了。所以便在盘算着买一台示波器的事情。</p>
<p>因为我肯定是没啥能力去搞真·高速 PCB, 按照之前的惯例，一台 100MHz 的示波器就足够我调试我能够设计的几乎全部电路了。正好最近碰到鼎阳的机器在打折，其中 SDS804X HD 这个型号看起来相当有吸引力：</p>
<ul>
<li>4 通道输入</li>
<li>70MHz 前端†</li>
<li>2GSa/s 采样率</li>
<li>12-bit 分辨率</li>
<li>50Mpts 存储深度</li>
</ul>
<p>以及：</p>
<ul>
<li>†用一点小手段，你可以解锁最高 200MHz, 因为实际上这个东西只是软件锁，模拟前端都一样的。</li>
</ul>
<p>只要 2k 出头，这谁看了不迷糊。我是迷糊了好一阵子，等我反应过来的时候示波器已经送到门口了。</p>
<h2>你说的那个小手段……</h2>
<p>首先，还是请大家有经济能力的情况下，正经支持一下国产仪器仪表。以及，一旦你选择破解示波器固件，你可能会<strong class="strong-cjk">失去保修和校准资格</strong>。如果你需要做任何严肃的测量，尤其是在商用或更严格的环境下测量，请直接购买满足你的需求的硬件以获取对应的仪表校准保证。</p>
<p>再强调一遍：这个小手段<strong class="strong-cjk">无法保证破解后的测量精度一定满足指标要求</strong>。就和解锁 CPU 被封锁的核心一样，这个操作本身就存在不确定的风险——示波器厂家完全可以把模拟前端性能不满足 200MHz 指标但是满足 70MHz 指标的板子装给标称 70MHz 的示波器用；在这种情况下破解示波器绝对会导致测量出现问题。</p>
<p>当你执行破解过程的时候，一定要检查输入的命令是否正确、核对解锁码是否正确。虽然没试过输入错误的激活码会导致什么后果，但<strong class="strong-cjk">最差的后果永远是锁机</strong>。你总不希望刚到手的机器就得寄回去修吧。</p>
<p>我收到的示波器固件版本是 <strong>1.1.6.5</strong>. 我不能保证这个小手段在之后的固件版本仍能起效，所以祝君好运。</p>
<p>例行警告结束，我们进入正题。</p>
<p>参考 <a href="https://thinkalone.win/SDS804X-HD-Upgrade.html">Disappear9</a> 和 <a href="https://www.eevblog.com/forum/testgear/siglent-sds804x-hd-same-upgrade-scripts/">EEVBlog 论坛</a>上各路大神的方案，需要先准备这些东西：</p>
<ul>
<li>一根网线，以及局域网</li>
<li>在局域网的电脑，装好 Python；手机的话你得自己想办法跑 Python 脚本</li>
</ul>
<p>玩法如下：</p>
<ol>
<li>通过网线将示波器连接到局域网</li>
<li>示波器端，选择功能 - 菜单... - 接口设置 - 局域网配置</li>
<li>勾选 IP 配置自动获取，确认此对话框，等待示波器连接到网络</li>
<li>关闭对话框再重新打开即可看到示波器的 IP 地址</li>
<li>在电脑上通过浏览器直接打开示波器的 IP 地址即可进入示波器管理页面</li>
<li>进入 Home 页面，记下示波器的序列号 (SN) 备用，它应该是 SDS 开头的</li>
<li>进入 SCPI 命令页面</li>
<li>键入 SCPI 命令 <code class="inline">MD5_SRLN?</code> 并发送，获取示波器的序列号备用</li>
<li>修改附录中的 Python 脚本对应位置，并运行脚本以获取各个模式的解锁码</li>
<li>键入 SCPI 命令 <code class="inline">MCBD?</code> 查询当前示波器的解锁码，它应该和 <code class="inline">70M</code> 一行的码值一致；如果不一致说明 <code class="inline">SCOPEID</code> 和 <code class="inline">SN</code> 输入有误，需要修正。一定要注意不要修改大小写或者引入多余的空格</li>
<li>找到 <code class="inline">100M</code> 一行对应的码，键入 SCPI 命令 <code class="inline">MCBD 100M激活码</code> 先将系统应用到 100M 的状态；注意把 <code class="inline">100M激活码</code> 替换成实际输出的值，比如 <code class="inline">MCBD 3VN1234567890HY5</code></li>
<li>操作示波器，选择功能 - 存储/调出... - 调出；类型选择「出厂设置」，然后选择「调出」选项在 100M 模式下恢复出厂设置</li>
<li>恢复成功后，回到电脑端，键入 SCPI 命令 <code class="inline">MCBD 200M激活码</code>；同理需要把 <code class="inline">200M激活码</code> 替换成脚本输出的实际激活码</li>
<li>操作示波器，再次按照第 12 步的方法恢复出厂</li>
<li>拔掉示波器的所有探头</li>
<li>操作示波器，选择功能 - 菜单... - 维护 - 自校正，确认开始校准过程</li>
<li>校准过程会需要跑一段时间，一般要跑五六分钟，有时候会跑更久，坐和放宽</li>
<li>校准完成后，选择功能 - 重新启动</li>
<li>示波器重启完成后，在系统信息里便会显示自己的型号是 SDS824X HD 了</li>
</ol>
<p>至于它会不会解锁 824X 型号的 100Mpts 存储深度，俺也不确定。</p>
<p>如果你还需要激活 PA 选件，那么做如下操作：</p>
<ol>
<li>操作示波器，选择功能 - 菜单... - 软件选件</li>
<li>下拉菜单选择 PA 选件，然后输入破解脚本 <code class="inline">PWA</code> 行对应的序列号即可；注意大小写</li>
<li>确认即可激活选件，重启即可</li>
</ol>
<h2>Python 脚本和样例运行输出</h2>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> hashlib

<span class="hljs-comment"># SCPI页面运行命令 MD5_SRLN? 获得SCOPEID</span>
SCOPEID = <span class="hljs-string">'0000000000000000'</span>

<span class="hljs-comment"># 在 Home 页面找到SN进行替换</span>
SN = <span class="hljs-string">'SDS00000000000'</span>

<span class="hljs-comment"># 以下内容不要动</span>
Model = <span class="hljs-string">'SDS800X-HD'</span>

bwopt = (<span class="hljs-string">'70M'</span>, <span class="hljs-string">'100M'</span>, <span class="hljs-string">'200M'</span>)
otheropt = (<span class="hljs-string">'PWA'</span>,)

hashkey = <span class="hljs-string">'5zao9lyua01pp7hjzm3orcq90mds63z6zi5kv7vmv3ih981vlwn06txnjdtas3u2wa8msx61i12ueh14t7kqwsfskg032nhyuy1d9vv2wm925rd18kih9xhkyilobbgy'</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">gen</span>(<span class="hljs-params">x</span>):
    h = hashlib.md5((
        hashkey +
        (Model+<span class="hljs-string">'\n'</span>).ljust(<span class="hljs-number">32</span>, <span class="hljs-string">'\x00'</span>) +
        opt.ljust(<span class="hljs-number">5</span>, <span class="hljs-string">'\x00'</span>) +
        <span class="hljs-number">2</span>*(((SCOPEID <span class="hljs-keyword">if</span> opt <span class="hljs-keyword">in</span> bwopt <span class="hljs-keyword">else</span> SN) + <span class="hljs-string">'\n'</span>).ljust(<span class="hljs-number">32</span>, <span class="hljs-string">'\x00'</span>)) + <span class="hljs-string">'\x00'</span>*<span class="hljs-number">16</span>).encode(<span class="hljs-string">'ascii'</span>)
    ).digest()
    key = <span class="hljs-string">''</span>
    <span class="hljs-keyword">for</span> b <span class="hljs-keyword">in</span> h:
        <span class="hljs-keyword">if</span> (b &lt;= <span class="hljs-number">0x2F</span> <span class="hljs-keyword">or</span> b &gt; <span class="hljs-number">0x39</span>) <span class="hljs-keyword">and</span> (b &lt;= <span class="hljs-number">0x60</span> <span class="hljs-keyword">or</span> b &gt; <span class="hljs-number">0x7A</span>):
            m = b % <span class="hljs-number">0x24</span>
            b = m + (<span class="hljs-number">0x57</span> <span class="hljs-keyword">if</span> m &gt; <span class="hljs-number">9</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0x30</span>)
        <span class="hljs-keyword">if</span> b == <span class="hljs-number">0x30</span>:
            b = <span class="hljs-number">0x32</span>
        <span class="hljs-keyword">if</span> b == <span class="hljs-number">0x31</span>:
            b = <span class="hljs-number">0x33</span>
        <span class="hljs-keyword">if</span> b == <span class="hljs-number">0x6c</span>:
            b = <span class="hljs-number">0x6d</span>
        <span class="hljs-keyword">if</span> b == <span class="hljs-number">0x6f</span>:
            b = <span class="hljs-number">0x70</span>
        key += <span class="hljs-built_in">chr</span>(b)
    <span class="hljs-keyword">return</span> key.upper()

<span class="hljs-built_in">print</span>(<span class="hljs-string">'--------------------------------'</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">'\n'</span>)
<span class="hljs-keyword">for</span> opt <span class="hljs-keyword">in</span> bwopt:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'{:5} {}'</span>.<span class="hljs-built_in">format</span>(opt, gen(SCOPEID)))

<span class="hljs-built_in">print</span>(<span class="hljs-string">'\n'</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">'--------------------------------'</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">'\n'</span>)
<span class="hljs-keyword">for</span> opt <span class="hljs-keyword">in</span> otheropt:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'{:5} {}'</span>.<span class="hljs-built_in">format</span>(opt, gen(SN)))
</code></pre>
<p>样例运行输出（具体的解锁码已隐去）：</p>
<pre><code class="hljs language-text">$ python unlock.py
--------------------------------


70M   CAP__________P3F
100M  3VN__________HY5
200M  2MG__________3RP


--------------------------------


PWA   89T__________PXM
</code></pre>
</body></html>]]></description>
<pubDate>Sat, 15 Aug 2026 16:55:47 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>杰理 WTS 格式音频转换和打包</title>
<link>/archives/jieli-wts-compiler.html</link>
<description><![CDATA[<html><head></head><body><p>tldr: .mp3/.wav -(ffmpeg)-&gt; .pcm -(wtgv2_encode.exe)-&gt; .wts -(packres.exe) -&gt; .cfg</p>
<p></p><p></p>
<p>一般而言，你应该使用 JLStudio 那个玩意从提示音选项卡中直接生成资源文件的。但如果你的用例比较特殊（比如多项目共享同一个仓库），那么就得自己跑整个流程。</p>
<p>以下是根据 JLStudio 的行为通过 Procmon 抓事件分析得出的过程。这个过程可能随着后续更新而有所修改。</p>
<p>这些工具都需要你想办法能安装 JLStudio. 你可以没有任何 SDK 授权，主要是里面的工具文件 <code class="inline">C:\JLStudio\tools\JLTools</code> 需要使用。除了 <code class="inline">ffmpeg.exe</code> 是原版 FFmpeg 以外，其他的工具在网上似乎都没有公开提供。</p>
<p></p><p></p>
<p>首先，将你的音频文件转成 16kHz 的裸 PCM, 格式为小端序 16bit 有符号数。这个部分可以通过 FFmpeg 做，或者你也可以自己写一个程序来搞。用 FFmpeg 转换的指令为：</p>
<pre><code class="hljs language-bash">ffmpeg.exe -y -i audio.mp3 -f s16le -acodec pcm_s16le -ac 1 -ar 16000 audio.pcm
</code></pre>
<p>WAV 格式的音频同理，只需要替换输入文件名，保留其他参数即可。</p>
<p>得到 PCM 数据后，再调用 <code class="inline">wtgv2_encode.exe</code> 把它编码成杰里的 WTS 格式：</p>
<pre><code class="hljs language-bash">wtgv2_encode.exe audio.pcm audio.wts o.raw 16000 16000 20 0 0
</code></pre>
<p>其中，各个参数的推测意义如下：</p>
<table>
<thead>
<tr>
<th>参数位置</th>
<th>意义</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>输入文件名</td>
<td>可以是一个绝对路径</td>
</tr>
<tr>
<td>2</td>
<td>输出文件名</td>
<td>可以是一个绝对路径，如果目标已经存在，则它会被覆写</td>
</tr>
<tr>
<td>3</td>
<td>???</td>
<td>固定为 <code class="inline">o.raw</code>, 暂时没抓到其他的情况</td>
</tr>
<tr>
<td>4</td>
<td>输入采样率？</td>
<td>目前观察到的值固定是 <code class="inline">16000</code>, 不确定是否支持其他采样率</td>
</tr>
<tr>
<td>5</td>
<td>输出采样率？</td>
<td>同上；设备端是否支持其他采样率也需要调查</td>
</tr>
<tr>
<td>6</td>
<td>???</td>
<td>固定为 <code class="inline">20</code>, 暂时没抓到其他情况</td>
</tr>
<tr>
<td>7</td>
<td>???</td>
<td>固定为 <code class="inline">0</code>, 暂时没抓到其他情况</td>
</tr>
<tr>
<td>8</td>
<td>压缩偏好</td>
<td><code class="inline">0</code> 表示质量优先；<code class="inline">1</code> 表示大小优先</td>
</tr>
</tbody>
</table>
<p>得到 WTS 文件后，还需要将其打包成 <code class="inline">.cfg</code> 文件才能喂给烧录器下载。使用 <code class="inline">packres.exe</code> 即可：</p>
<pre><code class="hljs language-bash">packres.exe -n audio -o audio.cfg audio.wts:audio.wts
</code></pre>
<p>其中，<code class="inline">-n</code> 参数指定一个名称（至于这个名称是啥作用我还在摸索），<code class="inline">-o</code> 参数指定输出文件名；之后的每一项参数都是一个由冒号分隔的打包条目，冒号左手侧是资源文件路径，右手侧是机上文件名称。</p>
<p>注意到：<code class="inline">packres.exe</code> 会拒绝文件名超过 11 个字节的文件。所以在之前的生成 WTS 的步骤中，输出文件名的长度必须控制在 11 个字节以内。</p>
<p>考虑到命令行最大参数限制，<code class="inline">packres.exe</code> 还额外接受从 <code class="inline">.txt</code> 中按行读取音频文件。考虑如下的文件 <code class="inline">audios.txt</code></p>
<pre><code class="hljs language-txt">D:\work\jl\proj\res\audio1.wts|audio1.wts
D:\work\jl\proj\res\audio2.wts|audio2.wts
</code></pre>
<p>则可以通过以下命令指示 <code class="inline">packres.exe</code> 按此文件打包音频：</p>
<pre><code class="hljs language-bash">packres.exe -n audio -o audio.cfg -pipe-sep -utf8-list -list audios.txt
</code></pre>
<p>其中，<code class="inline">-pipe-sep</code> 指定使用 <code class="inline">|</code> 分隔打包指令；如果你要使用绝对路径的话，一定要加这个参数；<code class="inline">-utf8-list</code> 指定输入的文件是 UTF-8 编码的，这个主要是为了应对潜在的非 ASCII 路径问题，可按情况选择加或者不加；<code class="inline">-list</code> 参数指定输入列表文件的路径。</p>
</body></html>]]></description>
<pubDate>Fri, 14 Aug 2026 04:54:02 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>[11ty] 增量式地刷新 CDN 缓存</title>
<link>/archives/11ty-managing-cdn-caches-again.html</link>
<description><![CDATA[<html><head></head><body><p>计算机科学中困难的事情有两件：缓存管理、取名，以及差一错误。</p>
<p></p><p></p>
<p>刚刚上期我们讨论了<a href="/archives/11ty-managing-cdn-caches.html">如何在 11ty 中将对资产文件的修改传播到内容页</a>。但是结尾发现 CDN 会缓存页面文件本身，使得我们必须要手动刷新页面缓存才能实际应用这个更改。</p>
<p>之前的架构中，由于涉及到 Docker 镜像的推送和拉取，对 CDN 的操作需要异步地进行。由于 <a href="https://github.com/containrrr/watchtower/discussions/2135">Watchtower 已经终止维护</a>，再加上感觉并没有必要专门为了一个静态博客上这么复杂的架构，所以我决定撤掉 Docker, 直接使用 Nginx 返回静态页面。</p>
<h2>通过 <code class="inline">rsync</code> 部署站点</h2>
<p>如果没有需要重写以前 WordPress 站点 URL 的历史包袱的话，完全可以将整个站点通过 <code class="inline">rclone</code> 放到对象存储中；或者直接用各种 Pages 方案托管整个站点，这样连 Nginx 都不需要设置。</p>
<p>但是拖着需要用一堆 <code class="inline">rewrite</code> 保持不知道在哪的老 URL 还能用的我最终选择了用 <code class="inline">rsync</code> 将编译产物推到服务器上。至少比 <code class="inline">scp</code> 来的强一点。</p>
<p>首先，先在服务器上安装 <code class="inline">rsync</code>. 没错，是在服务器上安装 <code class="inline">rsync</code>. 在通过 <code class="inline">rsync</code> 同步文件时，需要起一个服务端的 <code class="inline">rsync</code> 来比对和解压文件。</p>
<p>新建一个系统用户作为 <code class="inline">rsync</code> 的登录用户：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> useradd -r rsync
</code></pre>
<p>为 <code class="inline">rsync</code> 设置登录密钥：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /home/rsync/.ssh
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> -R rsync:rsync /home/rsync
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 700 /home/rsync/.ssh
</code></pre>
<p>生成一份 SSH 密钥供 <code class="inline">rsync</code> 用户使用：</p>
<pre><code class="hljs language-bash">ssh-keygen -t ed25519 -C <span class="hljs-string">'rsync@myserver.arpa'</span>
</code></pre>
<p>为了配置方便，私钥密码 (passphrase) 部分可以留空（直接敲回车）：</p>
<pre><code class="hljs language-text">Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/user/.ssh/id_ed25519): /home/user/rsync_key
Enter passphrase for "/home/user/rsync_key" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/user/rsync_key
Your public key has been saved in /home/user/rsync_key.pub
The key fingerprint is:
SHA256:jeDIoknecb4KHf8Zu9PavXoBf1j5jzuwC935zGi69t4 rsync@dsstudio.tech
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|                 |
|      .       .  |
|   . o . +   o   |
| ..oo.. S + o .  |
|ooo.*      =.o o |
|oo o o .. . +oo..|
|  .   o.=..oo +*.|
|   ... *+oo+=B=oE|
+----[SHA256]-----+
</code></pre>
<p>我们将<strong class="strong-cjk">公钥</strong>的内容写入服务器端的 <code class="inline">rsync</code> 用户授权的密钥中：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">cat</span> rsync_key.pub | <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> /home/rsync/.ssh/authorized_keys
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> rsync:rsync /home/rsync/.ssh/authorized_keys
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 600 /home/rsync/.ssh/authorized_keys
</code></pre>
<p>同时，我们需要限制一下 <code class="inline">rsync</code> 用户通过 SSH 的登录权限：</p>
<pre><code class="hljs language-text"># 追加到 /etc/ssh/sshd_config 尾部
Match User rsync
    PubKeyAuthentication yes # 打开公钥登录
    X11Forwarding no         # 禁止 X11 流量转发
    AllowTcpForwarding no    # 禁止 TCP 流量转发
    PermitTTY no             # 禁止交互式登录
</code></pre>
<p>编辑 CI 流程。由于我在用 Forgejo, 所以我需要明确地指定一些步骤来自于 GitHub 镜像；如果你在用 GitLab 或者 Gitea 则 <code class="inline">uses</code> 项目应该写 <code class="inline">burnett01/rsync-deployments@v9</code>.</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">deploy-site</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">builder:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 前略，之前的步骤构建好了站点</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">the</span> <span class="hljs-string">Thing</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">https://github.com/burnett01/rsync-deployments@v9</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">switches:</span> <span class="hljs-string">-avzr</span> <span class="hljs-string">--delete</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">_site/</span>
          <span class="hljs-attr">remote_path:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.RSYNC_REMOTE_PATH</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">remote_host:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.RSYNC_REMOTE_HOST</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">remote_user:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.RSYNC_REMOTE_USER</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">remote_key:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.RSYNC_REMOTE_KEY</span> <span class="hljs-string">}}</span>
</code></pre>
<p>为 CI 设置以下秘密变量：</p>
<ul>
<li><code class="inline">RSYNC_REMOTE_PATH</code>: 要推送到的绝对路径，包含末尾斜杠，比如 <code class="inline">/srv/http/</code></li>
<li><code class="inline">RSYNC_REMOTE_HOST</code>: 你的服务器 IP 地址</li>
<li><code class="inline">RSYNC_REMOTE_USER</code>: 通过 SSH 登录的用户名，就是刚刚设置好的 <code class="inline">rsync</code></li>
<li><code class="inline">RSYNC_REMOTE_KEY</code>: 登录 SSH 使用的私钥</li>
</ul>
<p>你的私钥应该有如下的格式：</p>
<pre><code class="hljs language-text">-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
（中间内容省略）
Q/3FHReMNjmJStfqUxzpAAAAE3JzeW5jQGRzc3R1ZGlvLnRlY2gBAg==
-----END OPENSSH PRIVATE KEY-----
</code></pre>
<p>添加完成后，如果你不计划再配置其他项目，则最好删除私钥文件：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">shred</span> -u rsync_key
</code></pre>
<p>完成之后便可以推到 Git 上验证 CI 流程能否正常将站点推送到服务器上了。</p>
<h2>刷新 EdgeOne 的缓存</h2>
<p>我是用腾讯云的 EdgeOne 作为站点 CDN 的。如果你在使用其他云服务，则需要自行接入对应的 API SDK.</p>
<p>安装腾讯云的 NodeJS SDK:</p>
<pre><code class="hljs language-bash">npm i tencentcloud-sdk-nodejs
</code></pre>
<p>按照 ESM 的方式导入并调用 EdgeOne 的缓存清除任务：</p>
<pre><code class="hljs language-js"><span class="hljs-comment">// in scripts/teo-purge-pages.js</span>
<span class="hljs-keyword">import</span> tencentcloud <span class="hljs-keyword">from</span> <span class="hljs-string">'tencentcloud-sdk-nodejs'</span>

<span class="hljs-keyword">const</span> <span class="hljs-title class_">TeoClient</span> = tencentcloud.<span class="hljs-property">teo</span>.<span class="hljs-property">v20220901</span>.<span class="hljs-property">Client</span>

<span class="hljs-keyword">const</span> clientConfig = {
  <span class="hljs-attr">credential</span>: {
    <span class="hljs-attr">secretId</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">TENCENTCLOUD_SECRET_ID</span>,
    <span class="hljs-attr">secretKey</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">TENCENTCLOUD_SECRET_KEY</span>,
  },
  <span class="hljs-attr">region</span>: <span class="hljs-string">''</span>,
  <span class="hljs-attr">profile</span>: {
    <span class="hljs-attr">httpProfile</span>: {
      <span class="hljs-attr">endpoint</span>: <span class="hljs-string">'teo.tencentcloudapi.com'</span>,
    },
  },
}

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TeoClient</span>(clientConfig)

<span class="hljs-keyword">let</span> params = {
  <span class="hljs-title class_">ZoneId</span>: <span class="hljs-string">'*'</span>,
  <span class="hljs-title class_">Type</span>: <span class="hljs-string">'purge_prefix'</span>,
  <span class="hljs-title class_">Method</span>: <span class="hljs-string">'invalidate'</span>,
  <span class="hljs-title class_">Targets</span>: [ <span class="hljs-string">'https://blag.dsstudio.tech'</span> ]
}

client.<span class="hljs-title class_">CreatePurgeTask</span>(params).<span class="hljs-title function_">then</span>(
  <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(data)
  },
  <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">'error'</span>, err)
  }
)
</code></pre>
<p>执行此脚本时，需要配置环境变量 <code class="inline">TENCENTCLOUD_SECRET_ID</code> 和 <code class="inline">TENCENTCLOUD_SECRET_KEY</code>. 这两个 API 密钥可以在账户配置里获取。推荐是新建一个 RAM 子账户、仅分配 EdgeOne 的操作权限并禁止管理后台登录。</p>
<p>要在 CI 中调用，则可以通过秘密变量传递到对应的环境变量：</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">builder:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 前略</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Purge</span> <span class="hljs-string">the</span> <span class="hljs-string">CDN</span> <span class="hljs-string">Cache</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">purge-pages</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">TENCENTCLOUD_SECRET_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TENCENTCLOUD_SECRET_ID</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">TENCENTCLOUD_SECRET_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TENCENTCLOUD_SECRET_KEY</span> <span class="hljs-string">}}</span>
</code></pre>
<p>提交成功后，会输出类似如下信息：</p>
<pre><code class="hljs language-text">{
  RequestId: 'e4db7ad5-315c-470c-b3c4-02fe78d1b780',
  FailedList: [],
  JobId: '3tlugj3jud1f'
}
</code></pre>
<h2>获取实际更新的文件表</h2>
<p>由于每次编译都会导致产物的文件时间和服务器上存放的文件时间不同，这导致了每次构建站点都会全量替换所有的文件。这肯定不是我们需要的。</p>
<p>修改 <code class="inline">rsync</code> 的调用方式：要求检查文件摘要 (<code class="inline">-c</code>) 并忽略时间戳 (<code class="inline">--no-times</code>)</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">builder:</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 前略</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">the</span> <span class="hljs-string">Thing</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">https://github.com/burnett01/rsync-deployments@v9</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">switches:</span> <span class="hljs-string">-avczr</span> <span class="hljs-string">--delete</span> <span class="hljs-string">--no-times</span>
          <span class="hljs-comment"># 后略</span>
</code></pre>
<p>但目前我们仍然是会刷新全站缓存，未免有点浪费，能不能只刷新真正被修改的文件呢？</p>
<p>要求 <code class="inline">rsync</code> 输出修改摘要到文件以便之后处理 (<code class="inline">--log-file</code>)：</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">builder:</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 前略</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">the</span> <span class="hljs-string">Thing</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">https://github.com/burnett01/rsync-deployments@v9</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">switches:</span> <span class="hljs-string">-avczr</span> <span class="hljs-string">--delete</span> <span class="hljs-string">--no-times</span> <span class="hljs-string">--log-file=rsync.log</span>
          <span class="hljs-comment"># 后略</span>
</code></pre>
<p><code class="inline">rsync.log</code> 会包含类似如下的输出：</p>
<pre><code class="hljs language-text">2026/08/10 08:05:56 [22] building file list
2026/08/10 08:05:57 [22] .d..t...... ./
2026/08/10 08:05:57 [22] .f..t...... feed.xml
2026/08/10 08:05:57 [22] .f..t...... index.html
2026/08/10 08:05:57 [22] &lt;fc.t...... sitemap.xml
2026/08/10 08:05:58 [22] *deleting   pagefind/pagefind.zh_fa64fcd6ad.pf_meta
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_facb98c.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_f5e7107.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_e5caa93.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_e51ff77.pf_index
</code></pre>
<h2>最小化地刷新缓存</h2>
<p>有了 <code class="inline">rsync.log</code>, 就可以从中解读哪些文件受到影响了。</p>
<p>对于每行日志信息，抛去时间戳前缀后，我们需要关心以下两种输出：</p>
<ul>
<li>以 <code class="inline">&lt;f</code> 开头的，表示传输到服务器的文件</li>
<li>以 <code class="inline">*delete</code> 开头的，表示从服务器上移除</li>
</ul>
<p>那么，我们只需要按行读取日志、按照上述条件筛选出受影响的文件即可：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">compileAffectedFileList</span>(<span class="hljs-params">content</span>) {
    <span class="hljs-keyword">const</span> modifiedFiles = rsyncLogLines
        .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> parts = line.<span class="hljs-title function_">split</span>(<span class="hljs-string">' '</span>)
            <span class="hljs-keyword">const</span> flags = parts[<span class="hljs-number">3</span>]
            <span class="hljs-keyword">const</span> file = parts.<span class="hljs-title function_">slice</span>(<span class="hljs-number">4</span>).<span class="hljs-title function_">join</span>(<span class="hljs-string">' '</span>)
            <span class="hljs-keyword">return</span> { flags, file }
        })
        .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> !!line.<span class="hljs-property">flags</span> &amp;&amp; !!line.<span class="hljs-property">file</span>)
        .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> line.<span class="hljs-property">flags</span>.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">'&lt;f'</span>))
        .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> line.<span class="hljs-property">file</span>)
    <span class="hljs-keyword">const</span> deletedFiles = rsyncLogLines
        .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> parts = line.<span class="hljs-title function_">split</span>(<span class="hljs-string">' '</span>)
            <span class="hljs-keyword">const</span> flags = parts[<span class="hljs-number">3</span>]
            <span class="hljs-keyword">const</span> file = parts.<span class="hljs-title function_">slice</span>(<span class="hljs-number">4</span>).<span class="hljs-title function_">join</span>(<span class="hljs-string">' '</span>)
            <span class="hljs-keyword">return</span> { flags, file }
        })
        .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> !!line.<span class="hljs-property">flags</span> &amp;&amp; !!line.<span class="hljs-property">file</span>)
        .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> line.<span class="hljs-property">flags</span>.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">'*deleting'</span>))
        .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">line</span>) =&gt;</span> line.<span class="hljs-property">file</span>)
    <span class="hljs-keyword">const</span> affectedFiles = [...modifiedFiles, ...deletedFiles]
    <span class="hljs-keyword">return</span> affectedFiles
}
</code></pre>
<p>读取日志文件：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { readFileSync } <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>
<span class="hljs-keyword">import</span> { exit } <span class="hljs-keyword">from</span> <span class="hljs-string">'process'</span>

<span class="hljs-keyword">const</span> rsyncLog = <span class="hljs-title function_">readFileSync</span>(<span class="hljs-string">'rsync.log'</span>, <span class="hljs-string">'utf-8'</span>)
<span class="hljs-keyword">const</span> affectedFiles = <span class="hljs-title function_">compileAffectedFileList</span>(rsyncLog)

<span class="hljs-keyword">if</span> (affectedFiles.<span class="hljs-property">length</span> === <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// 没有受到影响的文件，可以直接跳过刷新</span>
    <span class="hljs-title function_">exit</span>(<span class="hljs-number">0</span>)
}
</code></pre>
<p>然后组织并提交：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title class_">TeoClient</span> = tencentcloud.<span class="hljs-property">teo</span>.<span class="hljs-property">v20220901</span>.<span class="hljs-property">Client</span>
<span class="hljs-keyword">const</span> clientConfig = { <span class="hljs-comment">/* 从略 */</span> }
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TeoClient</span>(clientConfig)

<span class="hljs-keyword">let</span> params = {
  <span class="hljs-title class_">ZoneId</span>: <span class="hljs-string">'*'</span>,
  <span class="hljs-title class_">Type</span>: <span class="hljs-string">'purge_prefix'</span>,
  <span class="hljs-title class_">Method</span>: <span class="hljs-string">'invalidate'</span>,
  <span class="hljs-title class_">Targets</span>: affectedFiles.<span class="hljs-title function_">map</span>(
    <span class="hljs-function">(<span class="hljs-params">path</span>) =&gt;</span> <span class="hljs-string">`https://blag.dsstudio.tech/<span class="hljs-subst">${path}</span>`</span>
  )
}

client.<span class="hljs-title class_">CreatePurgeTask</span>(params).<span class="hljs-title function_">then</span>(
  <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(data)
  },
  <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">'error'</span>, err)
  }
)
</code></pre>
<p>容易想到，或许 <code class="inline">Targets</code> 一次可以提交的数量是有限的；我们会需要分批提交这个数据：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> batchSize = <span class="hljs-number">100</span>
<span class="hljs-keyword">const</span> batches = <span class="hljs-title class_">Array</span>.<span class="hljs-title function_">from</span>({ <span class="hljs-attr">length</span>: <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">ceil</span>(affectedFiles.<span class="hljs-property">length</span> / batchSize)}, <span class="hljs-function">(<span class="hljs-params">_, i</span>) =&gt;</span> affectedFiles.<span class="hljs-title function_">slice</span>(i * n, (i + <span class="hljs-number">1</span>) * n))
<span class="hljs-keyword">const</span> jobs = batches.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">batch</span> =&gt;</span> client.<span class="hljs-title class_">CreatePurgeTask</span>({
    <span class="hljs-title class_">ZoneId</span>: <span class="hljs-string">'*'</span>,
    <span class="hljs-title class_">Type</span>: <span class="hljs-string">'purge_prefix'</span>,
    <span class="hljs-title class_">Method</span>: <span class="hljs-string">'invalidate'</span>,
    <span class="hljs-title class_">Targets</span>: batch.<span class="hljs-title function_">map</span>(
        <span class="hljs-function">(<span class="hljs-params">path</span>) =&gt;</span> <span class="hljs-string">`https://blag.dsstudio.tech/<span class="hljs-subst">${path}</span>`</span>
    )
}))
<span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>(jobs).<span class="hljs-title function_">then</span>(<span class="hljs-variable language_">console</span>.<span class="hljs-property">log</span>).<span class="hljs-title function_">catch</span>(<span class="hljs-variable language_">console</span>.<span class="hljs-property">error</span>)
</code></pre>
<p>这样，每次就只会刷新收到影响的页面啦！</p>
</body></html>]]></description>
<pubDate>Mon, 10 Aug 2026 14:18:38 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>站起来，为你的成果答辩吧</title>
<link>/archives/stand-up-and-defend-your-work.html</link>
<description><![CDATA[<html><head></head><body><p>我劝你谨言慎行。</p>
<p></p><p></p>
<p>最近这句话又开始缭绕在我的心头。尽管我完全可以简单地扔一句「圈子越大，神人越多；圈子越小，神人越神」然后继续该干啥干啥，但是这一次我却觉得有点小小的心酸。或许是因为在对方身上看到了自己的影子，看到了那个贪多嚼不烂的自己；看到了那个渴望得到认可的自己；看到了那个想要成为学者的自己；看到了那个被批得血淋淋的自己；看到了那个灰心丧气的自己。</p>
<p>疼么？也许吧，但这也是过程的一部分。也许后面会做得更好，也许后面会识趣地沉默。沉默也许是一种最省事的方法：它不会带来不必要的关注，而且也不需要我再花费更多精力想办法让自己被看见。但选择了沉默，也就选择了消沉。我想给过去的那个自己一个交代。</p>
<p>任何尝试过整理资料的人都会明白这绝非喝茶看报那样的闲情逸事；而编译资料则更需要投入。本就小众的选题，加上本就匮乏的资料，又让这默默付出多了一份辛劳。</p>
<p>但世界并不关心你的苦劳。残忍地说，世界并没有任何义务来关心你的苦劳。世界只关心你的工作的最终产物。这种纯粹的结果导向会产生这样一个问题：</p>
<p><strong class="strong-cjk">是否应该认为所有公开发表的数据都要经得起检验和批评？</strong></p>
<p>这里的「数据」并不单单指代表格或者数据库条目，它指代广义上的信息——这篇博文就是一种数据；一条短视频也是一种数据；一段播客也是一种数据；它也并不仅仅局限于电子信息，一本书、一幅画、一张照片，都是数据。之所以不直接称之为信息，是因为我认为数据是一种客观存在而信息是主观的产物：数据只有经过解读之后才会成为信息。</p>
<p>这个问题，从理工科背景似乎很容易回答：「是」，那不然呢？</p>
<p></p><p></p>
<p>有了数据，便会有解读数据的方法。</p>
<p>数据自离开作者的手时，其解释的权利就几乎毫无保留地交给了读者。而读者会以不同的方式去解读相同的数据，进而得出不同的结论。一份「结构完好」的数据，理应得到大差不差的解读结果；然而在解读过程中，我们相当容易引入非理性的因素，进而产生一些新的解读。我需要强调一下「非理性」并不是一件坏事：有时我们必须引入非理性的因素才能解读数据，比如自身的生活经验就是一种非理性的因素。</p>
<p>这些新的解读可能与作者在构建数据时的意图正交。「草鱼的眼里闪过诡异的光」，到底是什么意思？它不是一份实验报告、不是一份专利说明、不是一份操作手册、不是一份项目申请、不是一本干到啃不动的教科书，它不是一份可以通过机械的理性来理解的数据——机械的理性会告诉你草鱼的眼睛是无法发光的，结合当时的环境描写，鱼的眼睛或许是反射了火光，而火光的飘忽不定或许贡献了「闪过」和「诡异」。</p>
<p>但作者在写这一部分的时候，或许压根就不是这样考虑的。你我作为杀出 K-12 教育血海的人，就算再厌恶语文，就算再对文字不感冒，也会有一种「应该另有所指」直觉。可是指什么呢？我没有答案，因为我从没有经历过故事中描写的物质的贫乏，所以我无法从中自然地推断出这种关于幸福的割裂；这种关于生活的诡异；这种……藏在无法被序列化的非理性组件中的体验。</p>
<p>是否应该认为，所有公开发表的数据，都要经得起检验和批评？</p>
<p>工科的训练告诉我：你应该为你的每一份数据负责，所以哪怕你不发表，也应该要能经得起检验和批评。但是这有一个隐含的条件，就是所有人都会遵循着同一个解读框架来检验和批评你的数据。我们都能认同：使用酸碱滴定的数据分析方法来分析表 2-7 中所列出的材料在不同应力下的形变量是荒谬的；甚至，我们都能认同：用正态分布去建模一类设备的平均故障时间是荒谬的。</p>
<p>但是即使是工科内部，对同一份数据仍旧能够通过不同的模型给出不同的解读；我相信在其他的学科中，每个人选用的解读框架更会有所不同；而走出书本之后，我甚至无法确定他人是否会有某种可被理解的解读数据的框架。</p>
<p>不要忘了，数据是一种客观存在：它可以是事实，也可以是观点。事实的解读尚且不是非黑即白，观点的解读或许要超越 CIE 1931 所可以描述的色彩空间。</p>
<p>那么，我是否应该做好这份数据被以最意想不到的方式解读的打算？我是否能够做好我的数据被曲解的预案？</p>
<p>我是否有勇气面对一个不同频的世界？</p>
<p></p><p></p>
<p>或许可以认为「结构完好」是一个大前提，而大部分时间我所编撰的数据总是结构损坏的：它几乎总是恒定地缺失了一个解读框架。我会下意识地认为：所有人都会采用我所使用的框架，因而重复地声明它是毫无必要的。</p>
<p>那么，如何足够简练地展示数据解读的框架，再展示数据本身？这会是一个更好的方案么？</p>
<p>站起来，为自己的成果，向并不熟悉你的领域的人们答辩吧。</p>
</body></html>]]></description>
<pubDate>Sun, 09 Aug 2026 12:17:04 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>公理还是定理？</title>
<link>/archives/axiom-or-theorem.html</link>
<description><![CDATA[<html><head></head><body><p>一个十分有意思的脑力锻炼小游戏（并不小）。</p>
<p></p><p></p>
<div class="dialog-wrapper"><div class="dialog-container"><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:18</p><p class="dialog-irc-link"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> [Link:Axiom or Theorem?,<a href="https://www.youtube.com/watch?v=wewdTOrQ1nQ">https://www.youtube.com/watch?v=wewdTOrQ1nQ</a>]</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 找到了一个很有意思的视频</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> rickroll again?</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 我没那么无聊</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> tldw: 一个游戏；首先我们定义某种数学结构，然后轮流给出一个命题</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 如果这个命题无法通过已有的公理推出，那么这个命题就作为新的公理加入</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 如果这个命题可以通过已有的公理推出，那么这个命题就是一个定理，然后给出定理的人就输了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 那么如果我给的命题和既有公理矛盾呢？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 那么这个就不算数，你得再想一个新的命题</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 我能无限重试么？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 为了所有人的身心健康，不行</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 在构建的过程中，如果新的公理加入后可以推出旧的公理呢？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 这是允许的</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 来一局？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 那你最好能脑内渲染 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext>TeX</mtext></mrow><annotation encoding="application/x-tex">\TeX</annotation></semantics></math></span></span></span> 公式</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 啥时候微信支持 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext>TeX</mtext></mrow><annotation encoding="application/x-tex">\TeX</annotation></semantics></math></span></span></span> 渲染了？？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 这盘饺子可就是为了这碟子醋包的</p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:24</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 定义集合上封闭的二元运算 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>×</mo></mrow><annotation encoding="application/x-tex">\times</annotation></semantics></math></span></span></span> 和常量 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> let the game begin, you go first</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> \sigh{}</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>y</mi><mo>×</mo><mi>x</mi></mrow><annotation encoding="application/x-tex">\forall x, y: x \times y = y \times x</annotation></semantics></math></span></span></span></p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:25</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 上来就给这么好的性质么</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo>:</mo><mi mathvariant="normal">∃</mi><mi>y</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">\forall x: \exists y: x \times y = a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>a</mi><mo mathvariant="normal">≠</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">\forall x: x \times a \neq a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:27</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 这个好像矛盾吧</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 因为 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo>:</mo><mi mathvariant="normal">∃</mi><mi>y</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">\forall x: \exists y: x \times y = a</annotation></semantics></math></span></span></span> 所以存在一个 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi></mrow><annotation encoding="application/x-tex">y</annotation></semantics></math></span></span></span> 使得 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">a \times y = a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 然后又有 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>y</mi><mo>×</mo><mi>x</mi></mrow><annotation encoding="application/x-tex">\forall x, y: x \times y = y \times x</annotation></semantics></math></span></span></span> 所以 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>y</mi><mo>×</mo><mi>a</mi><mo>=</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">y \times a = a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 那就改成 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>a</mi><mo>=</mo><mi>a</mi></mrow><annotation encoding="application/x-tex">\forall x: x \times a = a</annotation></semantics></math></span></span></span></p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:29</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 好，现在我们有交换律、逆和幺元了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo separator="true">,</mo><mi>z</mi><mo>:</mo><mi>x</mi><mo>×</mo><mo stretchy="false">(</mo><mi>y</mi><mo>×</mo><mi>z</mi><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">(</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo stretchy="false">)</mo><mo>×</mo><mi>z</mi></mrow><annotation encoding="application/x-tex">\forall x, y, z: x \times (y \times z) = (x \times y) \times z</annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 有点没招了，这不阿贝尔群么</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∃</mi><mi>u</mi><mo separator="true">,</mo><mi>v</mi><mo separator="true">,</mo><mi>w</mi><mo>:</mo><mi mathvariant="normal">∀</mi><mi>x</mi><mo>:</mo><mi>x</mi><mo>=</mo><mi>u</mi><mo>∨</mo><mi>x</mi><mo>=</mo><mi>v</mi><mo>∨</mo><mi>x</mi><mo>=</mo><mi>w</mi></mrow><annotation encoding="application/x-tex">\exists u, v, w: \forall x: x = u \vee x = v \vee x = w</annotation></semantics></math></span></span></span></p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:31</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 这么快就祭出大杀器了么</p><p class="dialog-irc-notice">&lt;*ChatServ&gt; 00:33</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 我觉得应该没法再加其他的公理了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 满足这些公理的结构只有 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">{</mo><mn>1</mn><mo separator="true">,</mo><mi>x</mi><mo separator="true">,</mo><msup><mi>x</mi><mrow><mo>−</mo><mn>1</mn></mrow></msup><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex"> \{1, x, x^{-1}\} </annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 所以还能再凹一个：<span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo>:</mo><mi>x</mi><mo>×</mo><mi>y</mi><mo>=</mo><mi>x</mi></mrow><annotation encoding="application/x-tex"> \forall x, y: x \times y = x </annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这样就会让它进一步退化成 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">{</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex"> \{1\} </annotation></semantics></math></span></span></span></p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这不直接成了必赢策略么</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 如果一句话就能把结构退化成 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">{</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex"> \{1\} </annotation></semantics></math></span></span></span>，那就没意思了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 所以或许我们应该加点限制条件</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 或者我们可以从某个更开放的结构开始</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 我觉得就我们目前的数学功底不太能支持我们这么干</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> \shrug{}</p></div></div></body></html>]]></description>
<pubDate>Fri, 07 Aug 2026 18:04:39 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>[11ty] 处理资产文件的 CDN 缓存问题</title>
<link>/archives/11ty-managing-cdn-caches.html</link>
<description><![CDATA[<html><head></head><body><p>最近在尝试改善这套博客主题的可访问性，这意味着我会经常需要调整样式和脚本。每次调整都得登录 CDN 后台重置资源文件的缓存，实在太麻烦了。有没有更简单的方法？</p>
<p></p><p></p>
<div class="dialog-wrapper"><div class="dialog-container"><p class="dialog-irc-notice">&lt;*ChatServ&gt; 你戳了戳 Bird</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> rt, send help</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 很简单，你只要给资源文件带上查询参数就可以了，比如加个版本号啥的</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 你的页面中只需要指定不同的版本号，自然就不需要手动刷新 CDN 缓存了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 我希望你没有自作聪明地去改 CDN 的缓存策略去忽略查询参数，不过最好检查一下有没有谁替你改了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 那岂不是我每改动一下 CSS 就得改动一下 HTML 里的查询参数？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 不想动手的话就得多动脑</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这个参数可以是一个时间戳，也可以是一个版本号，甚至可以是一个随机数</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 至于怎么选择，以及怎么让 11ty 替你干活，就看你的具体用例了</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> someone's calling me. toodles</p><p class="dialog-irc-notice">&lt;*ChatServ&gt; Bird 将状态设置为「会议中」</p></div></div><p>好吧，自己动手！</p>
<p>最符合直觉的设计参数自然是自增的版本号，比如 <code class="inline">?v=1</code>. 当然，这么做就会落入到之前的需要手动修改的问题里。所以我们需要想办法自动地生成设计参数。</p>
<p>一个比较理想的方案是使用文件本身的<span class="d-l10n-translate-template" data-original="Digest" data-translated="摘要值">摘要值</span>，这样有改动的文件会生成一个新的参数；而之前没有改动的文件就会保留原有的参数。这样一来，我们既不会因为每次小改动都导致 CDN 需要重新拉取全部的资源文件；也不需要手动指定哪些文件的查询参数需要修改。</p>
<p>在 11ty 下，不同语言的模板之间可以通过 <code class="inline">renderFile</code> 指令混用。考虑到计算文件的<span class="d-l10n-translate-template" data-ref="摘要值">摘要值</span>会不可避免地用到 JavaScript, 我们就需要用到 11ty 的 JS 模板了。</p>
<p>新建一个文件 <code class="inline">lib.11ty.cjs</code> 作为生成对应 HTML 片段的模板。</p>
<p>从 NodeJS 中调取 <code class="inline">crypto</code> 来计算<span class="d-l10n-translate-template" data-ref="摘要值">摘要值</span>：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { createHash } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'crypto'</span>)

<span class="hljs-keyword">function</span> <span class="hljs-title function_">calculateHashValue</span>(<span class="hljs-params">str</span>) {
  <span class="hljs-keyword">const</span> hash = <span class="hljs-title function_">createHash</span>(<span class="hljs-string">'sha256'</span>);
  hash.<span class="hljs-title function_">update</span>(str);
  <span class="hljs-keyword">return</span> hash.<span class="hljs-title function_">digest</span>(<span class="hljs-string">'hex'</span>);
}
</code></pre>
<p>为了 URL 简短一些，我们可以只取<span class="d-l10n-translate-template" data-ref="摘要值">摘要值</span>开头的 8 位数：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">calculateShortHash</span>(<span class="hljs-params">str</span>) {
  <span class="hljs-keyword">return</span> <span class="hljs-title function_">calculateHashValue</span>(str).<span class="hljs-title function_">substring</span>(<span class="hljs-number">0</span>, <span class="hljs-number">8</span>);
}
</code></pre>
<p>再从 NodeJS 中调取 <code class="inline">fs</code> 来读文件内容：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { readFileSync, existsSync } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-keyword">function</span> <span class="hljs-title function_">calculateFileShortHash</span>(<span class="hljs-params">path</span>) {
  <span class="hljs-keyword">if</span> (!path) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>
  }

  <span class="hljs-keyword">if</span> (!<span class="hljs-title function_">existsSync</span>(path)) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>
  }

  <span class="hljs-keyword">return</span> <span class="hljs-title function_">calculateShortHash</span>(<span class="hljs-title function_">readFileSync</span>(path));
}
</code></pre>
<p>在我的 11ty 结构中，CSS 等资产文件是放在工程目录的 <code class="inline">assets/</code> 文件夹内，并直接复制到产物目录中的。这使得我可以直接写 <code class="inline">assets/css/style.css</code> 来在 HTML 和 11ty 编译时指代同一份文件，不需要做额外的转换。所以生成 URI 的函数也十分好写：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">generateLibraryIncludeUri</span>(<span class="hljs-params">path</span>) {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${path}</span>?v=<span class="hljs-subst">${calculateFileShortHash(path)}</span>`</span>
}
</code></pre>
<p>然后就是比较狂野的 JS/HTML 混成时间：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">importStyleSheet</span>(<span class="hljs-params">path</span>) {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;link rel="stylesheet" href="/<span class="hljs-subst">${generateLibraryIncludeUri(path)}</span>"&gt;`</span>
}

<span class="hljs-keyword">function</span> <span class="hljs-title function_">importScript</span>(<span class="hljs-params">path</span>) {
  <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;script src="/$generateLibraryIncludeUri(path)}&gt;&lt;/script&gt;`</span>
}
</code></pre>
<p>11ty 的 JavaScript 模板通过 <code class="inline">render</code> 函数返回渲染结果，我们只需要返回组装好的 HTML 字符串即可：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> stylesheets = [ <span class="hljs-string">'assets/css/style.css'</span>, <span class="hljs-string">'assets/css/icon.css'</span>, <span class="hljs-comment">/* ... */</span> ]
<span class="hljs-keyword">const</span> scripts = [ <span class="hljs-string">'assets/js/jquery.min.js'</span>, <span class="hljs-string">'assets/js/script.js'</span>, <span class="hljs-comment">/* ... */</span> ]

<span class="hljs-keyword">function</span> <span class="hljs-title function_">render</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> styleString = stylesheets.<span class="hljs-title function_">map</span>(importStyleSheet).<span class="hljs-title function_">join</span>(<span class="hljs-string">''</span>)
  <span class="hljs-keyword">const</span> scriptString = scripts.<span class="hljs-title function_">map</span>(importScript).<span class="hljs-title function_">join</span>(<span class="hljs-string">''</span>)
  
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${styleString}</span><span class="hljs-subst">${scriptString}</span>`</span>
}

<span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = render;
<span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span>.<span class="hljs-property">default</span> = render;
<span class="hljs-comment">// ^ 别忘了导出渲染函数</span>
</code></pre>
<p>在其他模板文件里即可使用，比如在 Liquid 模板下就可以按如下语法调用：</p>
<pre><code class="hljs language-liquid"><span class="hljs-template-tag">{% renderFile 'lib<span class="hljs-number">.11</span>ty<span class="hljs-operator">.</span>js' %}</span>
</code></pre>
<p>需要注意到：<code class="inline">renderFile</code> 的路径是相对于工程本身的，而不是相对于 <code class="inline">_include</code> 目录的。</p>
<h2>条件导入</h2>
<p>之前选择 11ty 的一个爽点就是可以通过<span class="d-l10n-translate-template" data-original="Front matter" data-translated="文件头">文件头</span>指定哪些库是必要的，尤其是比较沉的库（比如 Graphviz 或者 abc.js）就不需要每个页面都得加载。</p>
<p>但是 JavaScript 模板由于是通过 <code class="inline">renderFile</code> 调用的，不会自动收到文件头数据。我们需要将<span class="d-l10n-translate-template" data-ref="文件头">文件头</span>整理一下再转发过去。</p>
<p>在 <code class="inline">eleventy.config.js</code> 中新建一个 Liquid 函数：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> (<span class="hljs-params">eleventyConfig</span>) {
  eleventyConfig.<span class="hljs-title function_">addLiquidFilter</span>(<span class="hljs-string">'allData'</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">context</span>.<span class="hljs-title function_">getAll</span>()
  })
}
</code></pre>
<p>然后修改模板中的调用方式：</p>
<pre><code class="hljs language-liquid"><span class="hljs-template-tag">{% <span class="hljs-keyword">assign</span> renderData <span class="hljs-operator">=</span> <span class="hljs-built_in">page</span> <span class="hljs-operator">|</span> allData %}</span>
<span class="hljs-template-tag">{% renderFile 'lib<span class="hljs-number">.11</span>ty<span class="hljs-operator">.</span>js', renderData %}</span>
</code></pre>
<p>再修改一下 <code class="inline">lib.11ty.js</code> 来接收这个数据：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">render</span>(<span class="hljs-params">data</span>) {
  <span class="hljs-comment">// data 将包含文件头中的数据，比如 data.viz 对应文件头中 viz 的值</span>
}
</code></pre>
<p>这样，我们就可以根据文件头的信息动态地引入资产文件了：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> conditionalScripts = [
  {
    <span class="hljs-attr">predicate</span>: <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> !!data.<span class="hljs-property">viz</span>,
    <span class="hljs-attr">path</span>: <span class="hljs-string">'assets/js/viz-standalone.js'</span>,
  },
  {
    <span class="hljs-attr">predicate</span>: <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> !!data.<span class="hljs-property">viz</span>,
    <span class="hljs-attr">path</span>: <span class="hljs-string">'assets/js/viz-invoker.js'</span>,
  },
]

<span class="hljs-keyword">function</span> <span class="hljs-title function_">renderConditionalScript</span>(<span class="hljs-params">data</span>) {
  <span class="hljs-keyword">return</span> conditionalScripts
    .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.<span class="hljs-title function_">predicate</span>(data))
    .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">{ path }</span>) =&gt;</span> <span class="hljs-title function_">importScript</span>(path))
    .<span class="hljs-title function_">join</span>(<span class="hljs-string">''</span>)
}
</code></pre>
<h2>不过……</h2>
<div class="dialog-wrapper"><div class="dialog-container"><p class="dialog-irc-notice">&lt;*ChatServ&gt; Bird 将状态设置为「空闲」</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这样做其实会有一个问题</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 也不能算是问题吧，只能说是一种必然</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 因为你的内容页仍然缓存在 CDN 里，所以即使你修改了资产文件，它们也不会立刻应用到既有的页面上</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 所以在必要的情况下（比如修了什么恶性 bug 之后），你还是需要手动清理全站缓存</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 或者等页面缓存过期</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 那绕了这么一大圈，不还是白给</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 可以考虑让 CI 去清理缓存</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 不过就目前你的架构来看，单纯地在 CI 构建完成后清理缓存是不行的</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 应当是你的源站服务器更新好容器之后，再从 CDN 上清理缓存</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 话说你一个静态博客为啥要设计这么复杂的架构啊</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 因为我需要在不同的电脑上写东西？</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 所以肯定得用 git</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 所以自然就会通过 git 触发 CI 构建</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> Jenkins 太沉了所以就用了 Actions, 那么自然就会上 Docker 之类的各种东西</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> 许多实现并不是设计指派的，而是因为需求变化而自然演进出来的</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这玩意能跑起来本身就是计算机魔法的奇迹</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> why, thank you!</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> It's not a compliment.</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 这中间主要是需要等一个异步的 Docker 镜像更新太麻烦</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;Bird&gt;</span> 如果你能同步地部署这个站点，那么就可以把缓存清理集成到 CI 里面</p><p class="dialog-irc-text"><span class="dialog-irc-sender">&lt;dousha99&gt;</span> i'll try to figure that out</p></div></div></body></html>]]></description>
<pubDate>Wed, 05 Aug 2026 08:50:03 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>[11ty] 无障碍设计：正确地标识图和表</title>
<link>/archives/11ty-a11y-captioning-your-figures-and-tables.html</link>
<description><![CDATA[<html><head></head><body><p>纸上得来终觉浅，绝知此事要躬行。</p>
<p></p><p></p>
<h2>正确标识图</h2>
<p>在 Markdown 中插入一张图片需要使用如下语法：</p>
<pre><code class="hljs language-markdown">![<span class="hljs-string">alt</span>](<span class="hljs-link">src</span>)
</code></pre>
<p>不过貌似大部分人会把 <code class="inline">alt</code> 部分留空，这是不够理想的。一个具有完整语义元素的图片至少需要一个 <code class="inline">src</code> 属性和一个 <code class="inline">alt</code> 属性；<code class="inline">src</code> 用于指定加载图像的 URI, 而 <code class="inline">alt</code> 属性则用于说明图片的内容。</p>
<h3><code class="inline">alt</code> 部分应该写什么？</h3>
<p><code class="inline">alt</code> 部分应该对图片做出功能性描述。</p>
<p>想象你在给一位盲人朋友描述图片里的内容，这就是应该写到 <code class="inline">alt</code> 里的内容。总得来说，图片描述应该满足以下要求：</p>
<ul>
<li>用词简练</li>
<li>功能优先于内容</li>
<li>先总体后局部</li>
<li>描述图片中的内容而非图片本身
<ul>
<li>尤其是不要写「关于某某的图片」，直接写「某某」就足够了</li>
</ul>
</li>
<li>不要携带无法从图片中推断出的信息</li>
</ul>
<p>如果图片中包含文字且对于图片内容的解读有影响，则也需要转写图片内的文字。</p>
<h3>我一定要给每张图片都写上几百字的小作文嘛？</h3>
<p>当然不用。就像我们不会一股脑地将所有信息全部罗列出来一样，图片描述也不需要将图片中的所有内容都全部描述出来以至于你可以直接把这段文字当做提示词喂给图片生成 AI 然后得到一张看起来和原图几乎一样但是又各种不对味的图片。</p>
<p>图片描述需要和你所要表达的内容相辅相成。我们应将图片描述视作正文的扩展，这样即使你在一个没有网络的环境里或者在纯文本环境下也可以通过图片描述文字来理解图片所需要呈现的信息。试考虑这样一张图片：</p>
<figure>
    <img src="https://static.dsstudio.tech/public/picture/PortraitOfCommodoreGraceMHopper.jpg" alt="Portrait of Grace M. Hopper" width="300">
    <figcaption>Grace M. Hopper</figcaption>
</figure>
<p>在不同的场景下会需要编写不同的图片说明。<a href="https://webaim.org/techniques/alttext/">WebAIM 的指南</a>详细地列出了各种情况下应该如何编写图片的描述文字。不过，作为博客文章，我们一般只需要考虑以下 4 种可能的场景。</p>
<h4>场景 1: 独立图片</h4>
<p>图片单独地作为正文出现，周围没有说明文字，如同悬挂在展厅里的一幅画。</p>
<p>在这种情况下，你可能确实需要写一段话来详细地描述图片的内容，例：</p>
<pre><code class="hljs language-text">格蕾丝・霍珀（Grace Hopper）的正式半身军装肖像照，面向镜头拍摄，背景为纯净浅灰白色墙面。

画面左侧竖立着一面美国国旗，蓝底白星、红白条纹清晰可见，旗杆垂挂两根金黄色流苏穗带。

照片中心是年长白人女性格蕾丝・霍珀。她一头浅金色短发收拢在海军军官帽下。头戴美国海军黑白配色军官大檐帽：帽檐与帽侧为黑色，帽顶正面是米白色，帽子正中饰有金色刺绣徽章，图案为白头海雕、船锚与枝叶纹样。

她戴着一副眼镜，镜框上半部分深灰黑色，下半圈为浅米黄色透明材质。脸上布满皱纹，神情严肃沉稳，直视前方镜头。

她身着深藏青色美国海军军官礼服外套，外套配有圆形金色纽扣。内搭白色翻领衬衫，系黑色领结。

右胸（画面左侧）缝制长方形姓名铭牌，印有 “COMO HOPPER” 字样；左胸（画面右侧）整齐排列多条色彩缤纷的勋章勋表，带有红、蓝、金、粉等各色饰条。

整体画面光线柔和均匀，是官方标准军装人像摄影，氛围庄重正式。

格蕾丝・霍珀是著名数学家、计算机先驱、美国海军少将，编译器概念的开创者。
</code></pre>
<h4>场景 2: 作为说明性图片出现</h4>
<p>图片作为正文的补充出现。</p>
<p>这种情况下，只需要描述正文中所没有的内容即可；比如正文中已经对人物有了详细的生平介绍，则说明文字只需要描述图片中的内容，例：</p>
<pre><code class="hljs language-text">格蕾丝・霍珀的官方半身军装肖像。画面左侧是带有金色流苏的美国国旗，背景为素净浅白墙面。年长的她正视镜头，头戴美国海军军官帽，佩戴眼镜；身着深藏青色海军制服，内搭白衬衫、黑色领结，制服上配有姓名牌与一排彩色勋表，神态庄重沉稳。
</code></pre>
<h4>场景 3: 作为功能性图片出现</h4>
<p>图片参与到某种页面的交互功能中，如作为链接或按钮的一部分。</p>
<p>这种情况下，则需要优先描述图片的功能。注意到图片的功能并不是要写「这个图片是一个链接」，屏幕阅读器已经会先朗读类似「图片，链接，……」了，所以我们需要描述的是除了导航作用以外的功能。试考虑这样一个结构：</p>
<div class="dialog-bubble dialog-bubble-that">
  
  <div class="dialog-bubble-content">
    <a class="dialog-bubble-file-link" href="#">
      <div class="dialog-bubble-file-detail">
      <p class="dialog-bubble-file-name">神秘文件</p>
      <p class="dialog-bubble-file-size">1 MB</p>
      </div>
      <img class="dialog-bubble-file-icon" src="https://static.dsstudio.tech/public/icons/file-icon/pdf.png" alt="PDF 文件" data-view-image-ignore="">
    </a>
  </div>
</div>
<p>这里的 PDF 图标的功能是表示这个文件是一份 PDF 文件，则其图片描述就应该是「PDF 文件」。</p>
<h4>场景 4: 作为装饰性图片出现</h4>
<p>一些纯粹装饰性的图片，尤其是那种即使移除了对于正文也没有任何影响的图片，反而应该留一个空白的 <code class="inline">alt</code> 属性。这样，屏幕阅读器就会略过这些装饰图片。</p>
<p>对于分隔线等装饰性图片，应该优先考虑使用 CSS 的 <code class="inline">background-image</code> 属性来为不携带语义信息的元素指定图片，这样就不需要额外地考虑使用 <code class="inline">&lt;img&gt;</code> 标签所蕴含的潜在语义问题。</p>
<h3>添加图标题</h3>
<p>如果你受过写论文或者毕业设计说明的毒打，你可能会想着给图片添加一个标题。在 HTML 中，这样一个带着标题的图是一个 <code class="inline">&lt;figure&gt;</code>:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">figure</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://example.com/example.jpg"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">"Example image"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">figcaption</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">figcaption</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">figure</span>&gt;</span>
</code></pre>
<p>但是 Markdown 并不原生支持生成 <code class="inline">&lt;figcaption&gt;</code>, 所以我们需要通过一些手段来扩展 Markdown. 对于 11ty 方案来说，最常用的软件包是 <code class="inline">markdown-it-image-figures</code>.</p>
<p>从 NPM 召唤这个包：</p>
<pre><code class="hljs language-bash">npm i markdown-it-image-figures
</code></pre>
<p>然后修改 <code class="inline">eleventy.config.js</code> 导入：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> mdfg <span class="hljs-keyword">from</span> <span class="hljs-string">'markdown-it-image-figures'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> (<span class="hljs-params">eleventyConfig</span>) {
  eleventyConfig.<span class="hljs-title function_">amendLibrary</span>(<span class="hljs-string">'md'</span>, <span class="hljs-function">(<span class="hljs-params">md</span>) =&gt;</span> {
    md.<span class="hljs-title function_">use</span>(mdfg, {
      <span class="hljs-attr">figcaption</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// 将图片的 title 属性作为 &lt;figcaption&gt; 元素的内容</span>
      <span class="hljs-attr">lazy</span>: <span class="hljs-literal">true</span>,  <span class="hljs-comment">// 是否懒加载图片</span>
      <span class="hljs-attr">async</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// 是否允许异步加载图片</span>
    })
  })
}
</code></pre>
<p>然后我们就可以通过 Markdown 的图片 <code class="inline">title</code> 语法来指定一张图的标题了：</p>
<pre><code class="hljs language-markdown">![<span class="hljs-string">alt</span>](<span class="hljs-link">src "title"</span>)
</code></pre>
<h3>图标题的自动编号</h3>
<p>有了图标题，自然也得给图进行编号。得益于 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Counter_styles/Using_counters">CSS 3 中的计数器变量</a>，我们不需要挨个手打图 1 图 2 图 3, 只需要简单几行 CSS 就搞定了：</p>
<pre><code class="hljs language-css"><span class="hljs-selector-tag">body</span> { <span class="hljs-attribute">counter-reset</span>: figure-counter; }
<span class="hljs-selector-tag">figure</span> { <span class="hljs-attribute">counter-increment</span>: figure-counter; }
<span class="hljs-selector-tag">figcaption</span><span class="hljs-selector-pseudo">::before</span> {
  <span class="hljs-attribute">content</span>: <span class="hljs-string">"图 "</span> <span class="hljs-built_in">counter</span>(figure-counter) <span class="hljs-string">" "</span>;
}
</code></pre>
<p>不过，要在正文里像 <span class="math-inline"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext>TeX</mtext></mrow><annotation encoding="application/x-tex"> \TeX </annotation></semantics></math></span></span></span> 那样拿到图片的编号目前似乎还比较困难，而且目前似乎没有任何现成的解决方案，所以大概率还是需要用 Pandoc 或者 Bookdown 来实现这个效果——这就太沉了。</p>
<h3>有了图标题，还需要写图片的描述文字么？</h3>
<p>需要。图标题的作用是给图片赋予一个名字、为图片添加额外的说明（比如对图表数据的解读或者评论），以及添加来源或者版权信息的；而图片描述文字则是关于图片内容的描述。二者的作用理论上是不重叠的，所以最好不要逃课。</p>
<p>实在是嫌麻烦，可以选择不写图标题，毕竟并不是所有文章都得当论文来写。</p>
<h2>正确标识表</h2>
<p>Markdown 产生的表格一般是有正确的语义标签的：完整的 <code class="inline">&lt;thead&gt;</code> 和 <code class="inline">&lt;tbody&gt;</code> 元素用于描述表头。但是如果你的表需要将第一列作为表头的话，除了手写 HTML 使用 <code class="inline">&lt;th&gt;</code> 标签以外似乎并没有太好的方法。</p>
<h3>添加表标题</h3>
<p>表标题在 HTML 中使用 <code class="inline">&lt;caption&gt;</code> 标签，其语法如下：</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">caption</span>&gt;</span>Table caption<span class="hljs-tag">&lt;/<span class="hljs-name">caption</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">thead</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- 略 --&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- 略 --&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
</code></pre>
<p>如果不太想手写 HTML 的话，在 11ty 中，可以使用软件包 <code class="inline">markdown-it-table-captions</code> 为表格添加标题。</p>
<p>从 NPM 中召唤这个包：</p>
<pre><code class="hljs language-bash">npm i markdown-it-table-captions
</code></pre>
<p>然后修改 <code class="inline">eleventy.config.js</code> 导入：</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> mdtbl <span class="hljs-keyword">from</span> <span class="hljs-string">'markdown-it-table-captions'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> (<span class="hljs-params">eleventyConfig</span>) {
  eleventyConfig.<span class="hljs-title function_">amendLibrary</span>(<span class="hljs-string">'md'</span>, <span class="hljs-function">(<span class="hljs-params">md</span>) =&gt;</span> {
    md.<span class="hljs-title function_">use</span>(mdtbl)
  })
}
</code></pre>
<p>这个插件会识别表格紧贴的段落是否以 <code class="inline">Table:</code> 开头，并将其作为表格标题。没错，必须是 <code class="inline">Table:</code>, 必须是这个大小写，必须是英文冒号，冒号后面必须跟着一个英文空格，否则不管用。用例如下：</p>
<pre><code class="hljs language-markdown">| 项目 | 最小值 | 典型值 | 最大值 |
| --- | --- | --- | --- |
| 高电平阈值 (V) | - | 2.9 | - |
| 低电平阈值 (V) | - | 0.3 | - |
| 高电平保持时间 (ns) | 7.1 | - | - |
| 低电平保持时间 (ns) | 7.1 | - | - |
| 时钟频率 (MHz) | - | 64 | - |

Table: 设备的电器特性
</code></pre>
<p>编译后，便会得到这样一个表格：</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">caption</span>&gt;</span>设备的电器特性<span class="hljs-tag">&lt;/<span class="hljs-name">caption</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">thead</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>项目<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>最小值<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>典型值<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>最大值<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>高电平阈值 (V)<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>2.9<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>低电平阈值 (V)<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>0.3<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>高电平保持时间 (ns)<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>7.1<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>低电平保持时间 (ns)<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>7.1<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>时钟频率 (MHz)<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>64<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
</code></pre>
<p>我们也可以通过同样的 CSS 小技巧来指定表的编号：</p>
<pre><code class="hljs language-css"><span class="hljs-selector-tag">body</span> { <span class="hljs-attribute">counter-reset</span>: table-counter; }
<span class="hljs-selector-tag">table</span> { <span class="hljs-attribute">counter-increment</span>: table-counter; }
<span class="hljs-selector-tag">table</span> &gt; <span class="hljs-selector-tag">caption</span><span class="hljs-selector-pseudo">::before</span> {
  <span class="hljs-attribute">content</span>: <span class="hljs-string">"表 "</span> <span class="hljs-built_in">counter</span>(table-counter) <span class="hljs-string">" "</span>;
}
</code></pre>
<h4>指定表标题的位置</h4>
<p>默认情况下，表标题是出现在表格上面的，不过可能你的学校的习惯是表标题应该出现在表格的下面。令我惊讶的是，<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/caption-side">CSS 中专门有一个属性来指定表标题的位置</a>：</p>
<pre><code class="hljs language-css"><span class="hljs-selector-tag">table</span> &gt; <span class="hljs-selector-tag">caption</span> {
  <span class="hljs-attribute">caption-side</span>: bottom;
}
</code></pre>
<p>这样，即使不调整 <code class="inline">&lt;caption&gt;</code> 元素在 <code class="inline">&lt;table&gt;</code> 中的位置，浏览器也会自行将标题放在表格的底部了。</p>
</body></html>]]></description>
<pubDate>Sun, 02 Aug 2026 13:30:38 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>停止 Mac 上的时间机器备份</title>
<link>/archives/stopping-time-machines.html</link>
<description><![CDATA[<html><head></head><body><p>是时候停止一些无意义的操作了。</p>
<p></p><p></p>
<p>最近发觉家里的那台自建 NAS 的性能越来越差了，先是 <a href="/archives/migrating-database-for-freshrss.html">FreshRSS</a> 开始撞到网关超时，然后是 <a href="/archives/moving-into-forgejo.html">Gitea</a> 也越跑越慢。虽然迁移到 PostgreSQL 能勉强续一秒，但这总归不是个好事。</p>
<p>直觉上是因为整个存储阵列要被塞满了，所以越来越多的读写负载会发生在那对容量最大的磁盘上；加上现在又有多个服务都会频繁产生写操作，尽管有 PostgreSQL 做缓存，机械硬盘也不能很好地接住这些频繁的随机写。</p>
<p>如果打开 <code class="inline">kernel.task_delayacct</code><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> 并观察 <code class="inline">iotop</code> 的输出的话，会发现各个 PostgreSQL 产生的写入操作并不是很大，也并不是很频繁——虽然它们确实经常会出现 <code class="inline">100.00 %</code> 的 IO 等待时间，有时会卡住好几秒。</p>
<p>观察系统平均负载的话，容易看到按小时的涨落。有些大批量的硬盘写操作在周期性地发生。它也许是 FreshRSS 在大批量刷新订阅并写数据库；也许是系统中有一些维护任务在执行。但是 IOPS 却也随着写入的流量而一起涨落，而且经常摸到 1k 的数量级。如果是数据的写操作的话，它的 IOPS 应该更低一些才对。</p>
<p>这些模式指向了一个潜在的问题源：Mac mini 和 MacBook 的时间机器功能。只有这样一个玩意会每小时地产生大量读写操作，因为它需要比对旧数据、写入新的数据，又需要删除过时的数据。这自然会产生非常大的 IOPS. 我看着 <code class="inline">iotop</code> 上蹦出的几十行 <code class="inline">smbd</code> 的条目，觉得或许应该从这个方向排查一下。</p>
<p>时间机器是 macOS 系统自带的备份与还原功能，它可以连接到一个 SMB 共享文件夹，并展开一个特殊的文件夹结构 <code class="inline">.sparsebundle</code>. 它的内部结构模仿一个磁盘，里面由一些元数据和一个个「条带」组成。而每个「条带」……</p>
<pre><code class="hljs language-text"> dousha@dousha-box | ~ $ cd /mnt/volume/Backups
 dousha@dousha-box | /mnt/volume/Backups $ cd TimeMachine
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine $ ls
dousha  dousha-laptop-apple.sparsebundle  dousha-mac-mini.sparsebundle
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine $ cd dousha-mac-mini.sparsebundle
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine/dousha-mac-mini.sparsebundle $ ls
bands  com.apple.TimeMachine.MachineID.bckup  com.apple.TimeMachine.MachineID.plist  com.apple.TimeMachine.Results.plist  com.apple.TimeMachine.SnapshotHistory.plist  Info.bckup  Info.plist  lock  mapped  token
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine/dousha-mac-mini.sparsebundle $ cd bands
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine/dousha-mac-mini.sparsebundle/bands $ ls
0     114c  129a  13e8  1535  1683  17d1  191f  1a6d  1bbb  1d08  1e56  1fa4  20f4  2241  239   24de  262b  277b  28d   2a1e  2b6f  2cc   2e0d  2f62  30c0  3232  3381  34e8  3635  37a2  438  586  6d4  821  97   abe  c12  d63  eb2
&lt;300+ lines omitted&gt;
114b  1299  13e7  1534  1682  17d0  191e  1a6c  1bba  1d07  1e55  1fa3  20f3  2240  238f  24dd  262a  277a  28cf  2a1d  2b6e  2cbf  2e0c  2f61  30c   3231  3380  34e7  3634  37a1  437   585  6d3  820  96f  abd  c11  d62  eb1
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine/dousha-mac-mini.sparsebundle/bands $ file 114
114: data
 dousha@dousha-box | /mnt/volume/Backups/TimeMachine/dousha-mac-mini.sparsebundle/bands $
</code></pre>
<p>承载着不知道怎样格式的数据。也许是 HFS+ 或者 APFS 扇区数据之类的东西。</p>
<p>这意味着如果你没有一台跑着 macOS 的系统的话，想要从这坨二进制文件中提取出文件可能比打一枚能到月球的火箭还要难。</p>
<p>我突然觉得实际上我并不需要打开时间机器功能。</p>
<p>我在这些 Mac 上做的工作无外乎写写程序以及偶尔码一些不着边际的文字，多媒体的创作……我并没有足够的艺术细菌来支持，所以基本完全没有。而这些程序和文字自然已经是通过 <code class="inline">git</code> 良好组织了的。我没有任何理由去打开时间机器的恢复界面，因为 <code class="inline">git checkout</code> 来得更自然一些。</p>
<p>这些备份真的将永远不会用作恢复：Mac 的迁移过程可以在店里完成，所以压根用不到我的 NAS 上的一点内容；而如果我的 Mac 因为这样那样的原因炸掉，我又需要从备份里面调取文件的话，受限于果子的 <code class="inline">.sparsebundle</code> 格式，我必须再借一台 Mac 或者跑 macOS 虚拟机才能拿到我需要的东西。更不必提如果哪一天我决定离开果子的生态，那么这些数据就真的会变成一坨再也无法读取的毫无意义的二进制块。</p>
<p>配置时间机器是一个很头疼的事情。它没有 <code class="inline">.timemachineignore</code> 这样的文件，而且默认备份整个用户目录。虽然开箱即用很是方便，但这也使得各种开发环境产生的缓存文件和编译产物也会参与到备份当中，比如 <code class="inline">.m2</code>, <code class="inline">.nuget</code>, <code class="inline">vcpkg/</code>; 更不要说存在于各个工程目录里的 <code class="inline">.venv</code>, <code class="inline">node_modules/</code>, <code class="inline">dist/</code>, <code class="inline">out/</code>, <code class="inline">cmake-build-debug/</code>...</p>
<p>而即使我确实想要备份一些不适合扔到 <code class="inline">git</code> 里的东西，我也有另一个选择：<a href="https://www.urbackup.org/">UrBackup</a>； 或者如果我需要可以从脚本中调用的工具的话： <a href="https://restic.net/">restic</a>. 它们可以在本地文件有修改的情况下再执行备份过程，而不是每小时地产生一次维护动作；它们也可以动态地随着文件修改而立刻产生备份，而不需要等每小时一次的维护动作。</p>
<p>关闭时间机器的操作很简单，比我当初费劲调试 SMB 让它能开起来还要简单：在系统设置里删除 NAS 备份目标就可以了。当然，性能改善并非一劳永逸，我们还需要继续观察系统的运行情况。</p>
<hr><aside aria-labelledby="footnote-and-references"><h2 id="footnote-and-references">参考与注释</h2><ol><li id="fn1" class="footnote-item"><p><code class="inline">sudo sysctl kernel.task_delayacct=1</code> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol></aside></body></html>]]></description>
<pubDate>Fri, 24 Jul 2026 05:13:15 +0000</pubDate>
<dc:creator>dousha</dc:creator>
</item>

    
        
<item>
<title>从 Gitea 迁移到 Forgejo</title>
<link>/archives/moving-into-forgejo.html</link>
<description><![CDATA[<html><head></head><body><p>这次我们就没有 FreshRSS 那般的好运咯。</p>
<p></p><p></p>
<div style="border: 0.3em solid black; padding: 1em;">
<p>如果你已经在生产环境中使用 Gitea 而且这个 Gitea 实例不仅仅是你自己在用，那么<strong class="strong-cjk">不要</strong>尝试通过这个方式迁移你的环境：你会丢失所有的 Gitea Runner 秘钥信息、所有的软件包（比如私有 npm 包、私有容器镜像等）、所有的用户配置（比如头像、SSH 公钥、GPG 公钥等）、提交热力图和 Gitea 实例配置。</p>
<p>如果你确实需要迁移到不同的实例，那么请让用户在新的实例中重建他们的账户、将仓库迁移到新的实例，并重新配置 Runner, 并在一切都尘埃落定后再停止旧的实例！</p>
</div>
<p>由于最早的时候装 Gitea 时为了省事选了 SQLite 数据库，现在它也开始报 <code class="inline">504 Gateway Timeout</code> 了。解决方案自然是迁移到 PostgreSQL, 当然 Gitea 也是没法点两下鼠标就能更换数据库后端的。</p>
<p>简单翻了翻资料，这回是实打实的得重装整个实例才能切换数据库后端了。既然反正都要重装了，那……也许我们可以试试 <a href="https://forgejo.org/">Forgejo</a>? 反正这玩意也就我一个人在用，迁移 Git 托管后端应该不是一件困难的事情。</p>
<p>简单起一组容器：</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">forgejo:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">codeberg.org/forgejo/forgejo:15</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">forgejo</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">USER_UID=1000</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">USER_GID=1000</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">forgejo</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">forgejo:/data</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/etc/localtime:/etc/localtime:ro</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">logging:</span>
      <span class="hljs-attr">driver:</span> <span class="hljs-string">"json-file"</span>
      <span class="hljs-attr">options:</span>
        <span class="hljs-attr">max-size:</span> <span class="hljs-string">"100m"</span>
        <span class="hljs-attr">max-file:</span> <span class="hljs-string">"3"</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">3000</span><span class="hljs-string">:3000</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">222</span><span class="hljs-string">:22</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">m.daocloud.io/docker.io/postgres:16-alpine</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">forgejo_postgres</span>
    <span class="hljs-attr">shm_size:</span> <span class="hljs-string">128mb</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">forgejo</span>
      <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">password</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">forgejo</span>
<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">forgejo:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">false</span>
<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">forgejo:</span> <span class="hljs-literal">null</span>
</code></pre>
<p>安装时选择对应的数据库后端、按计划填好表即可。</p>
<p>接下来就到了愉快的迁移时间！作为程序员，面对这种简单的重复性劳动的第一反应当然是直接用脚本自动化——虽然也许手动戳一戳会快很多。</p>
<p>那么，先召唤两个服务的命令行工具：</p>
<pre><code class="hljs language-bash">yay -S tea forgejo-cli
</code></pre>
<p>首先在 Gitea 中创建一个最高权限的应用程序 Token, 留给之后迁移数据使用。我们也可以用这个 Token 从命令行中登录 Gitea:</p>
<pre><code class="hljs language-bash">tea login add
</code></pre>
<p>选择已有 Access token 并粘贴进去即可。</p>
<p>同样的，我们也要在 Forgejo 中创建一个最高权限的应用程序 Token.</p>
<p>相比起 <code class="inline">tea</code>, <code class="inline">fj</code> 就更为……传统一些。它对于参数的顺序是十分在意的，而且报错信息也相对而言比较隐晦。它需要你按照顺序指定 Forgejo 的实例位置，然后是命令，再然后是命令自己的各项参数；这中间不能随意换序。所以添加登录令牌就得是这么一个节奏：</p>
<pre><code class="hljs language-bash">fj -H http://forgejo.home.arpa auth add-key USERNAME_HERE forgejo-access-token
</code></pre>
<p>两边登录成功后就可以开始着手迁移了。设置好变量：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">export</span> GITEA_BASE_URL=<span class="hljs-string">"https://gitea.home.arpa"</span>
<span class="hljs-built_in">export</span> FORGEJO_BASE_URL=<span class="hljs-string">"http://USERNAME_HERE@forgejo.home.arpa"</span>
<span class="hljs-built_in">export</span> GITEA_ACCESS_TOKEN=<span class="hljs-string">"access-token"</span>
</code></pre>
<p>首先，创建所有的组织账号：</p>
<pre><code class="hljs language-bash">tea organizations <span class="hljs-built_in">ls</span> --<span class="hljs-built_in">limit</span> 99999 --page 1 |
<span class="hljs-built_in">tail</span> -n +4 |
<span class="hljs-built_in">head</span> -n -1 |
awk <span class="hljs-string">'{print $2}'</span> |
xargs -L 1 fj -H <span class="hljs-variable">${FORGEJO_BASE_URL}</span> org create
</code></pre>
<p>这个做法只创建了组织账号本身，组织账号剩下的属性，比如头像、描述等等东西如果仅通过 Shell 自带的能力非常容易搞炸，所以还是留给手动迁移或者正经的脚本吧。</p>
<p>然后仓库直接一把梭：</p>
<pre><code class="hljs language-bash">tea repos <span class="hljs-built_in">ls</span> --<span class="hljs-built_in">limit</span> 999999 --page 1 |
<span class="hljs-built_in">tail</span> -n +4 |
<span class="hljs-built_in">head</span> -n -1 |
awk <span class="hljs-string">"{print \"echo <span class="hljs-variable">${GITEA_ACCESS_TOKEN}</span> | fj -H <span class="hljs-variable">${FORGEJO_BASE_URL}</span> repo migrate -s gitea -t --include lfs,wiki,issues,prs,milestones,labels,releases <span class="hljs-variable">${GITEA_BASE_URL}</span>\" <span class="hljs-variable">$2</span> \"/\" <span class="hljs-variable">$4</span> \" \" <span class="hljs-variable">$2</span> \"/\" <span class="hljs-variable">$4</span> }"</span> |
xargs -I {} sh -c <span class="hljs-string">'{}'</span>
</code></pre>
<p>这么做会有一个很头大的问题：有些仓库在迁移时会报 <code class="inline">500 Internal Server Error</code>, 直觉上是因为源站响应时间过长导致的。所以脚本跑完了之后，还得再比对一下哪些仓库没能正确迁移成功，滤出之后再跑一遍——或者也可以选择反复执行这个命令，已经存在的仓库会报 <code class="inline">409 Conflict</code>.</p>
<p>完成仓库迁移后，就可以修改 Nginx 反代配置将 Git 托管服务指向 Forgejo 了。之前给 Gitea 的写的反代配置可以直接拿来用，只替换 <code class="inline">upstream</code> 就可以。</p>
<p>接下来就是 Runner 的迁移。</p>
<p>Runner 迁移没有方便的脚本，只能一个个去登录服务器、下载 Runner 程序并重新注册 Runner.</p>
<p>如果你不太喜欢搞 Docker in Docker 的话，推荐使用 Forgejo Runner 的独立程序版。以及这玩意的 Runner 配置需要手动操作的东西有点多，所以用独立程序版会相对而言更好操作一些。参考如下脚本下载独立程序：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">export</span> ARCH=$(<span class="hljs-built_in">uname</span> -m | sed <span class="hljs-string">'s/x86_64/amd64/;s/aarch64/arm64/'</span>)
<span class="hljs-built_in">export</span> RUNNER_VERSION=$(curl -X <span class="hljs-string">'GET'</span> https://data.forgejo.org/api/v1/repos/forgejo/runner/releases/latest | jq .name -r | <span class="hljs-built_in">cut</span> -c 2-)
<span class="hljs-built_in">export</span> FORGEJO_URL=<span class="hljs-string">"https://code.forgejo.org/forgejo/runner/releases/download/v<span class="hljs-variable">${RUNNER_VERSION}</span>/forgejo-runner-<span class="hljs-variable">${RUNNER_VERSION}</span>-linux-<span class="hljs-variable">${ARCH}</span>"</span>
wget -O forgejo-runner <span class="hljs-variable">${FORGEJO_URL}</span>
<span class="hljs-built_in">chmod</span> +x forgejo-runner
</code></pre>
<p>创建一个专门的应用程序用户：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> useradd --create-home forgejo
<span class="hljs-built_in">sudo</span> usermod -aG docker forgejo
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> forgejo-runner /home/forgejo/
</code></pre>
<p>只要不通过 <code class="inline">passwd</code> 指定这个用户的密码，那么它就无法远程登录。不过，我们可以 <code class="inline">sudo</code> 进去：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> -u forgejo -i
<span class="hljs-built_in">cd</span> ~
</code></pre>
<p>生成一份配置文件：</p>
<pre><code class="hljs language-bash">./forgejo-runner generate-config &gt; config.yml
</code></pre>
<p>Forgejo 的 Runner 支持各种容器环境；同样的，这也需要你指定需要使用的容器的运行环境以及每个标签对应使用什么镜像。如果需要使用 Docker, 那么需要修改配置文件中以下键值：</p>
<pre><code class="hljs language-yml"><span class="hljs-attr">container:</span>
  <span class="hljs-attr">docker_host:</span> <span class="hljs-string">"automount"</span>
</code></pre>
<p>然后指定使用以下镜像：</p>
<pre><code class="hljs language-yml"><span class="hljs-attr">runner:</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-latest</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ubuntu-24.04:docker://ghcr.io/catthehacker/ubuntu:act-24.04</span>
</code></pre>
<p>在 Forgejo 后台添加新的 Runner 后，后端会生成一段形如以下的配置文件片段：</p>
<pre><code class="hljs language-yml"><span class="hljs-attr">server:</span>
  <span class="hljs-attr">connections:</span>
    <span class="hljs-attr">forgejo:</span>
      <span class="hljs-attr">url:</span> <span class="hljs-string">https://forgejo.home.arpa/</span>
      <span class="hljs-attr">uuid:</span> <span class="hljs-string">1df4e318-9eda-4a2e-a6d7-8a905d2cc9f6</span>
      <span class="hljs-attr">token:</span> <span class="hljs-string">aaaabbbbccccddddeeeeffff000011112222</span>
</code></pre>
<p>我们只需要将它合并到对应 Runner 的配置文件中即可，不需要通过 <code class="inline">forgejo-runner register</code> 指令进行注册。</p>
<p>简单写一个 <code class="inline">systemd</code> 服务，使能即可：</p>
<pre><code class="hljs language-text">[Unit]
Description=Forgejo Runner
After=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/home/forgejo/forgejo-runner -c /home/forgejo/config.yml daemon
User=forgejo
Group=forgejo
WorkingDirectory=/home/forgejo

[Install]
WantedBy=multi-user.target
</code></pre>
<p>也许该抽时间做一下性能剖析：我知道现在磁盘阵列的访问很慢，可是究竟有多慢？有没有优化的方法？</p>
</body></html>]]></description>
<pubDate>Sun, 05 Jul 2026 03:02:50 +0000</pubDate>
<dc:creator>dousha</dc:creator>
</item>

    
        
<item>
<title>评论里的私信消息</title>
<link>/archives/secret-messages-in-comments.html</link>
<description><![CDATA[<html><head></head><body><p>在博客间随机游走的时候，无意间看到了一个博客的评论区中部分评论是带着锁的 <code class="inline">[Secret Message]</code>. 非常有趣。更有趣的是这个私密评论是站长回复其他人的评论——如果是其他人回复站长的私密评论实现起来应该不难，但是反过来的话，要怎么靠谱地实现这个东西呢？</p>
<p></p><p></p>
<p>容易知道这是一个 WordPress 站点。在 WordPress 下，实现私密评论的方法自然很多。毕竟有 PHP 作为中间层来隔离，我这种喜欢戳来戳去的人通常不会带来什么实质性的危害——后端只要返回尽可能有限的数据，比如这条评论只返回一个特殊的标记，正文什么都不返回就可以了。</p>
<p>不过，撬开开发者工具分析一下请求，发现这个评论区的请求并没有给这条私密评论安排一个特殊标记。它的内容就只是简单的 <code class="inline">[Secret Message]</code>, 而已。看来这个小锁图标应该是纯前端的工作了。（不过这样不会很易碎么？它确实很易碎啊，因为你的代码里确实是真的在 <code class="inline">=== '[Secret Message]'</code> 啊！）</p>
<p>抛开这个站点的实现不谈，如果说我需要实现这个功能，我们有哪些选项呢？</p>
<h2>L0. 我加密？我装的</h2>
<p>最简单的方案是：不实现。从后台拿到邮箱之后写一封邮件作为回复，然后再简单地往评论系统里塞一条模板消息表示俺回复过了就好了。<code class="inline">[Secret Message]</code> 什么的只是保持神秘感的桥段而已。</p>
<h2>L1. Cookie/LocalStorage</h2>
<p>我们或许可以借助 Cookie 来搞定这个问题：每位留言的访客都会收到一个 Cookie. 后台则可以根据 Cookie 信息来决定返回实际的内容或者返回提示文字。</p>
<p>但这么做的话有一个缺陷：如果访客换了一个浏览器或者换了一个设备，比如之前是用电脑发的评论，现在是在手机上看，那就只能干瞪眼了，因为 Cookie 变了。</p>
<p>这个缺陷当然也可以通过 OOB 的方法绕过，比如邮件通知里携带一个特殊的参数来设置 Cookie 或者解密；但这样的话还得小小地祈祷一下邮件不被扔到垃圾桶或者直接被退信。而且有些时候访客不愿意填一个真实的邮箱进来，那这封带着解密链接的邮件就彻底石沉大海了。</p>
<p>实际观察评论加载的行为时，我发现这个站点也并没有使用 Cookie. 不过，它确实在 LocalStorage 里面塞了一个访客 ID 给我。这个访客 ID 我简单翻找了一下，只和加载页面的文字有一些关系。当然，或许可能这个站点的实现是随着访客 ID 的不同，实际返回的前端脚本也会不一样，进而导致请求本身也会不一样？这么做显然是可以的，虽然对于服务器负载确实有点不友好。</p>
<p>要求访客为了能读评论而注册账号并登录显然有点不太现实，有没有更无感而且不依赖 Cookie 这种可能会要求你做一堆合规改造的方法呢？</p>
<h2>L2. SubtleCrypto</h2>
<p>现代浏览器里是可以通过 JS 做一些通行的密码学操作的，比如 <code class="inline">SHA-256</code> 和 <code class="inline">AES-256-CBC</code>. 它们都可以通过 <a href="https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto"><code class="inline">crypto.subtle</code></a> 来调取。</p>
<p>或许我可以把邮箱作为密钥，来加密这个评论。比如我可以取邮箱的 <code class="inline">SHA-256</code> 作为密钥，用户名的 <code class="inline">MD5</code> 作为 IV<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, 使用 <code class="inline">AES-256-CBC+PKCS#7</code> 算法来加密评论的正文。这样，其他人加载评论时只能看到类似于</p>
<pre><code class="hljs language-text">-----BEGIN ENCRYPTED MESSAGE-----
deMtJS/ZgZqrVt0hzXih1Q==
-----END ENCRYPTED MESSAGE-----
</code></pre>
<p>这样的内容；而如果在评论区的用户名栏中填写 <code class="inline">John Doe</code> 并邮箱栏中填写 <code class="inline">someone@example.com</code>, 则这个评论就会被前端脚本自动识别并解密成</p>
<pre><code class="hljs language-text">Hello, world!
</code></pre>
<p>这样我们就不需要浏览器端保存任何状态，也实现私密评论了；同时这样的数据甚至可以安全地缓存在 CDN 中，对服务器的负载也相当友好——前提是这位访客认真地填写了他的用户名和邮箱，而不是脸滚键盘随便生成了这些内容。这样的话那真的是谁都救不了。</p>
<p>当然，这么做会带来另一个比较头大的问题：很难保证邮箱本身没有泄露。评论旁边就挂着 Gravatar 的情况下，用一点小小的社工手段<a href="https://www.bleepingcomputer.com/news/security/online-avatar-service-gravatar-allows-mass-collection-of-user-info/">把邮箱解出来</a>或许并没有想象中那样困难。</p>
<p>我们当然可以让访客自己填一个密码之类的，但那样的话「注册账号」交互的阻力就又回来了。</p>
<p>Welp. That's why we can't have good things. 还是应该直接写邮件的。</p>
<h2>L3. 跨设备追踪？</h2>
<p>Eww... 但这事可能还真能成，因为广告 ID 可以提供另一层身份验证。我不知道你具体是谁，但是我知道你可能对什么感兴趣。如果我能拿到 32b 的数据，或者说了解你对于 32 样东西是否感兴趣，那么唯一地识别一个人应该是足够的。整个过程甚至不需要你动手，它在后台会自动完成。</p>
<p>除了广告 ID, 还有 IP 地址、粗略位置、浏览器窗体大小、系统中安装的字体、系统中可调用的程序、可用的环境传感器、加速计的漂移特征、利用超声波进行跨设备的数据传输……</p>
<p>想法很疯狂，但我应该是没啥技术实力来实现这个。况且也已经决定了不追踪，那么就还是说到做到。</p>
<hr>
<p>另：后端直接把所有该返回的不该返回的数据一并扔出来（比如明文的邮箱甚至是浏览器 UA），可能不是一个比较理想的选择。但我注意到代码里有检查邮箱具体值的一些操作，主要是用于标识站长……好吧，好吧，或许这是一个合理的答案。Thanks for the provisioning. <em>smile and wink</em></p>
<hr><aside aria-labelledby="footnote-and-references"><h2 id="footnote-and-references">参考与注释</h2><ol><li id="fn1" class="footnote-item"><p>我知道 MD5 不是一个安全的散列函数，但这总好过用一个固定的 IV. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol></aside></body></html>]]></description>
<pubDate>Mon, 22 Jun 2026 12:21:14 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>记得清理你的日志文件</title>
<link>/archives/snowballing-log-files.html</link>
<description><![CDATA[<html><head></head><body><p>尤其是如果你的 Docker 服务很喜欢打日志的话。</p>
<p></p><p></p>
<p>最近发现传家宝机器的磁盘空间不够用了，是 <code class="inline">Avail</code> 列显示 <code class="inline">0</code> 的那种不够用。虽然这个机器满打满算也就 60GB 的硬盘空间，但是上面也并没有跑什么大型服务：现在它只是一台跑着静态博客的机器——以及还跑着一个万年没更新的 Gitea 实例。1700 多天的 uptime 本可以更长——阿里云擅自重启了一次这台机器，重置了之前已经有 1000 天的 uptime 记录。</p>
<p><code class="inline">ncdu</code> 了一下 <code class="inline">~</code>, 并没有用多少磁盘空间，只有个位数 GB. 这意味着至少挂在 <code class="inline">~</code> 下的 Gitea 卷并没有多大，而且之前 Maven 和 NPM Cache 都已经清理过了，<code class="inline">~</code> 下面已经没有什么可以删除的了。</p>
<p>那么，或许是博客不断滚动更新的 Docker 镜像占用了太多空间？祭出 <code class="inline">docker image prune -a</code>. 但这条指令也只是释放了 1GB 多一点的空间。</p>
<p>或许问题出在别处。<code class="inline">ncdu</code> 了一下 <code class="inline">/var</code>, 看着一行行早已不再运行的服务滚过，心里也是五味杂陈。<code class="inline">/var/lib/jenkins</code> 下还陈列着过往的各个项目；<code class="inline">/var/lib/mysql</code> 还保存着各种实验留下的数据；这台机器上曾经发生过许多伟大的事情，不过那些都已经结束了。</p>
<p>突然，<code class="inline">ncdu</code> 的统计数字上跳了 20GB -- <code class="inline">/var/lib/docker</code> 下居然存在一个 20GB 大小的东西。仔细检查之后发现这个庞然大物是 Gitea 的 Docker 日志。这个玩意居然滚了 20GB 的日志出来！</p>
<p>好吧，之前写 <code class="inline">docker-compose.yml</code> 的时候没有给日志配置限额是我的问题：</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">gitea:</span>
    <span class="hljs-attr">logging:</span>
      <span class="hljs-attr">driver:</span> <span class="hljs-string">"json-file"</span>
      <span class="hljs-attr">options:</span>
        <span class="hljs-attr">max-size:</span> <span class="hljs-string">"100m"</span>
        <span class="hljs-attr">max-file:</span> <span class="hljs-string">"3"</span>
    <span class="hljs-string">...</span>
</code></pre>
<p>然后清理掉原先的日志文件：</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">truncate</span> -s 0 $(docker inspect --format=<span class="hljs-string">''</span> gitea)
</code></pre>
<p>再重新部署一下容器应用更改：</p>
<pre><code class="hljs language-bash">docker compose up -d
</code></pre>
<p>服务器硬盘空间不够的问题目前解决了。但紧接着的问题就是：怎么能搞出 20G 的日志文件的？除了单纯的「运行时间太久」？</p>
<p>还记得之前我说过<a href="/archives/2025300.html">大模型正在吞吃互联网</a>么？看来大模型的爬虫很喜欢在 Git 服务里面对着每个项目的 PR 和 Issue 页一顿猛输出呢。但问题在于：我已经有差不多一年没有往这个 Gitea 实例上推代码了；而且这上面所有项目都没有 PR 和 Issue. 无论再怎么排列组合查询参数，都不会有任何变化的。</p>
<p>但是这些爬虫很显然并不能理解这个问题。它们就会一直不断地重新排列组合各种查询参数来访问一个始终是空白的页面。它们也会孜孜不倦地用同样的参数反复请求同一个页面，就像一个一直在电脑前按 F5 期待有什么事情发生的人一样。</p>
<p>更头大的是，Gitea 默认是不带 <code class="inline">robots.txt</code> 来告诉爬虫不要反复做无谓的工作的——它甚至不带一些绝对必要的阻拦，比如别让搜索引擎爬取 <code class="inline">/api/</code> 或者搜索路径下的东西。</p>
<p>考虑到 Google 也发现了我还有个 Gitea 实例，而且从后台数据来看它也爬取了一堆排列组合，我只能先告诉 Google 不要爬取那些奇怪的东西：</p>
<pre><code class="hljs language-text">User-Agent: *
Disallow: /api
Disallow: /repo/search
Disallow: /explore/search
Disallow: /*/issues
Disallow: /*/issues/
Disallow: /*/pulls
Disallow: /*/pulls/
Disallow: /*/packages
Disallow: /*/packages/
Disallow: /*/projects
Disallow: /*/projects/
Disallow: /*/activity
Disallow: /*/activity/
Disallow: /*/blame
Disallow: /*/blame/
Allow: /
</code></pre>
<p>然后 WAF 安排掉 <code class="inline">GPTBot</code> 和 <code class="inline">Amazonbot</code> 这两个 UA. 至少现在它们两个还是会老老实实带上自己的大名的，之后就是看它们会不会伪装成普通浏览器继续骚扰了。</p>
</body></html>]]></description>
<pubDate>Mon, 22 Jun 2026 08:14:53 +0000</pubDate>
<dc:creator>dousha</dc:creator>
</item>

    
        
<item>
<title>招魂</title>
<link>/archives/in-search-of-a-missing-soul.html</link>
<description><![CDATA[<html><head></head><body><p>我不知道这是否是一种通病，所以忠实地记录并诚实地隐瞒，或许是当下最好的方案。</p>
<p></p><p></p>
<p><a href="/archives/the-ai-shock.html">前一阵子</a>谈到了关于精神世界中的小小冲击。不，我说的不是 AI 带来的震撼，而是这之前的冲击。现在该交代一下后文了：AI 并没能救下我，我也没能救下「我」。我最终将一切建模成了一场感染——具体而言，某种类似于结核分枝杆菌的感染，并将世界暂停在了早发现、早报告、早隔离的实践过程中——还没到早治疗，因为已经差不多确认没救了。</p>
<p>一方面是因为我发现我越继续推进，我的文字就越靠近那场倒了血霉的大疫。这虽然并非我本意，但毕竟，文字一离开创作者的手，它的意义就全由读者赋予了。虽然这份纯粹为了消解自身矛盾的文字应该是不会被公开发表，但是未来的我可能另有打算。</p>
<p>另一方面是，我发现除了把「我」关起来，现实世界的我也没有其他任何选择。我所处的环境并不能支持我做任何「有趣」的事情。稳定的接口是对接外部系统的基石，这既是一种祝福也是一种诅咒。他们会期待一个稳定的接口，任何变化都会引起不必要的关注。</p>
<p>但当我把「我」关起来之后，当我认为这样或许就可以解决眼前的问题之后，我突然发现：我实质上是在拒绝我自己。但我甚至不能确定被拒绝的真的是我自己。能够被称得上是「我」的那一部分，似乎<a href="/archives/type-ii-erasure.html">早已消失在历史的长河之中了</a>。而这个新出现的「我」，或者说在模型里，染上了疫病的「我」，他的历史又如何呢？</p>
<p>他当然没有比我更丰富的历史。或者更为务实地说，他因冲击而存在。如果没有这个冲击，那么他便压根不会存在；或者即使存在，也只是背景板的一部分。与其说他是「我」的创作者化身，不如说他是新思潮的载体，只是一具承载这「病菌」的躯壳罢了。</p>
<p>但这样推演的后果，便是承认这样一个事实：「我」也只是一具承载着社会功能的躯壳罢了，不然为什么一开始在创作出这代罪的羔羊时，我会用更小的小字写下 "creator's surrogate" 呢？</p>
<p>当我注视着我的内心时，发现里面早已空无一物。我已经失去了那个曾经鲜活过的「我」，变得单薄而枯槁了。这难道就是成长的代价么？这种「空心」的感觉，是正常的么？</p>
<p>我曾经和朋友打趣说，哎呀，减肥是真痛苦啊，饿得我魂都丢了。我并不是在开玩笑，我真的觉得我的灵魂不见了。</p>
<p>不过至少有一点可以确认：这不是因为饿疯了导致的问题。这两天我又整了点油盐糖混合大炸弹就着加了 B 族维生素的功能饮料吃了吃，不过这种阴郁的感觉也并没有随之消散。还好，至少这证明了阴郁的想法并不直接来自于神经系统的退行性改变。</p>
<p>但是这反而让我觉得更加烦闷。毕竟如果它只是我饿疯了的臆想，那么至少在减肥结束之时我可以自豪地说「我挺过来了」，我将迈步走向新的开始——至少我知道总有那么一天我的折磨将会正式结束，在这之前我可以放心大胆地对自己说「哦你一定是太饿了，不过没关系，我们一定能撑过去的」然后靠着复合维生素片继续生活下去。现在我只能对着不争气的自己叹气，质问自己为什么偏偏选择在此时此刻出问题。</p>
<p>翻了翻之前的记录，发现这个问题好像已经有一段时间了——有太长一段时间了！确切点来说，至少<a href="/archives/2599.html">五年前</a>我就已经感知到了虚拟化系统中存在的若干问题；至少<a href="/archives/4840.html">四年前</a>我就已经辨识到了「我是谁」这个问题的重要性。那时的我似乎还有一个魂，至少还能写出 <a href="/archives/2948.html">Ref6</a> 这样直白的剖析。</p>
<p>至于现在，现在的我只剩缄默，或者说，「诚实地隐瞒」。我像个机器一般，安静而缓慢地斟字酌句，将无法通过筛查的表述一一摘走，只剩下无菌而苍白的文书。</p>
<hr>
<p>为什么是某种类似结核分枝杆菌的感染？哦，因为：</p>
<ul>
<li>结核分枝杆菌可以通过飞沫传播；这对得上：这玩意真的是纯粹的模因危害，我认真的</li>
<li>结核是有隐性感染状态的，在这种状态下，携带者不发病，也不会传染其他人；这对得上：深柜自然不具有传染性
<ul>
<li>全球四分之一的人口都是携带者；这或许对得上，那样或许会让我好过一点</li>
<li>隐性感染可以持续很长时间；这对得上：<a href="/archives/2948.html">Ref6</a> 都已经是五年前的事情了</li>
<li>隐性感染状态实际上是因为免疫系统将结核菌包裹在肉芽肿中了，它实质上没有被消灭；这对得上：五年前我以为我解决了这个问题，但实际上并没有</li>
</ul>
</li>
<li>隐性感染可以在特定条件下转化为活动性肺结核；这对得上：不然也不会说是冲击了</li>
<li>即使是活动性结核，它也不会立刻致死，而是十分缓慢地发展；这对得上：我现在还没有完全 cave in, 但我无法预测未来如何，以及我还能撑多久。我咳嗽的时候会捂住口鼻，以及我会勤洗手，尽可能不带来太多问题</li>
<li>文学作品中对于结核病的浪漫描写还少么（笑）</li>
</ul>
<p>以及还有一件事情：每次做结核菌素试验的时候，我都是至少三个加号，所以每次都要拍片证明自己没有肺结核才行。按道理来说，卡介苗应该没有那么持久的威力吧，可是上了高中体检的时候我却还是四个加号，而且就我一个人这样，其他人都没啥事，这总让我感到一丝丝的不安。</p>
<p>未来的我或许会嘲笑现在的我的苦恼——这或许是件再好不过的事情。</p>
</body></html>]]></description>
<pubDate>Sun, 21 Jun 2026 15:03:25 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>Annihilation</title>
<link>/archives/annihilation.html</link>
<description><![CDATA[<html><head></head><body><p>我的第一本正经读完的英文小说。</p>
<p></p><p></p>
<p>考虑到这本小说应该是有中译本的，所以就把「湮灭」这个名字留给中译本了。英语原本<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>还是继续沿用 Annihilation 这个名字。</p>
<p>Annihilation 是……怪诞小说？不确定怎么翻译 Weird Fiction, 就暂且叫它「怪诞小说」好了。上来就搞这一口，可能还是得归结于之前 The Drabblecast 带来的「污染」。技术上来说，之前在 Drabblecast 上读的短篇英文小说按道理也能算是读英文小说；但是读正经印发成书的，这还是头一次。</p>
<p>大多数书单都会将 Annihilation 和 Perdido Street Station 列为当代怪诞小说的代表作。我想或许从现代小说入手不失为开始英语文学阅读的好手段。</p>
<p>和一般的「志怪小说」不同，怪诞小说对于「怪」的部分的描绘并不拘泥于传统的妖魔鬼怪，也不屑于一板一眼的正邪对抗的演出；怪诞小说所描绘的并不是纯粹的恐怖或者血浆震撼，但一定能让你感到惊奇；尤其是新怪诞体裁 (New Weird) 下，其多元化的叙事手法结合更贴近现实、更为复杂的世界模型所渲染的光怪陆离的图景总能让人沉浸于一种超脱的情感之中。</p>
<p>单从行文来说，Annihilation 并不是一部需要很高的词汇量或者对英语文学有一定了解之后才能欣赏的作品，至少不会有那种翻开 Great Expectations 刚看了两页纸就被噎得不行的感觉；但从内容上来说，Annihilation 是一部相当难以消化的作品，它的叙事手法决定了它可能无法给出传统奇幻小说中的可追寻的演进。我们可以跟随着主角的脚步前进，却并不能简单地通过主角的眼睛去观察世界，因为主角并不是一个「可靠叙述人 (Reliable Narrator)」。故事讲完之后，我们并不能获得一个完满的结论——一些读者甚至会认为这个故事只是结束而压根没有收尾，即使以「这是 Southern Reach 三部曲中的第一部，所以肯定会留有悬念」作为借口来说，Annihilation 也并没有回答完所有它所抛出的问题。</p>
<p>但如果选择接受怪诞小说中必然包含的超现实的描写，则可以认为 Annihilation 并没有「义务」回答所有问题，甚至，「Area X 究竟是什么」这种问题本身就不是这部小说所应该要回答的——给出一个具体的答案反而会破坏它；就像现在我们一谈到爱手艺就不可避免地想到大章鱼一样，那种本应是背离现实、超乎理解的概念反而和具象而俗套的现实绑定了。</p>
<p>那么，Annihilation 到底讲了什么故事？我或许只能诚实地说：「我不知道」。我当然可以复述一遍剧情梗概，但我的复述不会比其他人写得更好或者更详实，甚至可能也没法真的体现「我」的想法。而且梗概实在是难以捕捉文字中细腻的演进，恰恰这本书所描绘的又偏偏是这种细腻的演进！</p>
<p>这种如真菌菌落般的细腻盘根错节，如同书中反复出现的各种其他意象一样交织成了一幅幅难以琢磨的画面。很多时候你可能会觉得这部作品的行文过于缓慢和拖沓，伏笔和伪像又难以分辨，无数的问题涌来却没有答案，大段大段的独白让人不知所以；但如果愿意耐着性子继续读下去的话，会发现前文看似不经意的描写和对话此刻突然有了新的含义。</p>
<p>也许 Annihilation 讲述的是：当我们终将进入一个无法理解的世界时，我们还剩下哪些选项；也许它在讨论变化本身的永恒；也许它在凸显人性直面重大挫折时所折射出的光辉；也许是因为 Olsen 关于《湮灭 (2018)》的电影评论<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>先入为主地让我认为这是一个关于「自我的湮灭」的故事；当然，也许这只一段故弄玄虚的呓语罢了。</p>
<p>但是 Olsen 或许指出了一个关键的问题：《湮灭 (2018)》不是一部关于外星人的电影。基于此，或许也可以认为 Annihilation 不是一部关于未知边境的小说。如果我们拒绝从比喻意义上解读这些作品，固执地从表象上对作品进行批评，那么这些作品就只能是充满了漏洞的陈词滥调。</p>
<p>我在读这部作品的时候，思绪经常跑到《对镜方知身是客<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup> <sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>》那边，因为我能从文字中感受到主角一直在忠实地隐瞒一些事情，无论是有意还是无意。这种对于信息披露的克制，或许是一种通行的写作手法？抑或是我正尝试在字缝之间寻找答案但求而不得的一种失落？或者，二者都体现出了对于人类中心主义的质疑，所以我总是下意识地觉得它们熟悉又陌生？</p>
<p>Annihilation 是一本好书么？也许是吧，但它注定是一本无法提供答案的书，它注定是一本反过来诘问读者的书。我在写这篇文章的时候顺手搜了一下中译本的相关信息，才后知后觉地发现原来这本书是获得了 2014 年星云奖的小说——同年提名的还有《三体》。「打败了《三体》」的广告词未免有点……没品，但或许这就是我们目前所处的、一个并不在乎「我们」所知所想的、无情而绮丽的世界。</p>
<hr><aside aria-labelledby="footnote-and-references"><h2 id="footnote-and-references">参考与注释</h2><ol><li id="fn1" class="footnote-item"><p>VanderMeer J. Annihilation[M/TP]. MCD Picador. 2024R1+9781250824042. p1-195. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Olsen D. Annihilation and Decoding Metaphor[V/OL]. YouTube. (2018-11-01)[2026-06-21]. <a href="https://www.youtube.com/watch?v=URo66iLNEZw">https://www.youtube.com/watch?v=URo66iLNEZw</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>exurb1a. maybe I'm the alien[V/OL]. YouTube. (2026-05-08)[2026-06-21]. <a href="https://www.youtube.com/watch?v=y894m7joEBc">https://www.youtube.com/watch?v=y894m7joEBc</a>. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>exurb1a, 倪谅. 对镜方知身是客[V/OL]. Bilibili. (2026-05-31)[2026-06-21]. <a href="https://www.bilibili.com/video/BV1RKVQ6TE8V/">https://www.bilibili.com/video/BV1RKVQ6TE8V/</a>. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol></aside></body></html>]]></description>
<pubDate>Sun, 21 Jun 2026 11:21:24 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>[11ty] 使用 Pagefind 实现静态站点的搜索</title>
<link>/archives/11ty-searching-with-pagefind.html</link>
<description><![CDATA[<html><head></head><body><p>最近发现站点的搜索不太正常，我在需要查找之前的文章时发现按标题搜却怎么都搜索不到，哪怕是英语文章这种理论上不应该出现分词问题的文章也找不到。</p>
<p>那么，正好捋一下在 11ty 中需要怎么接入 Pagefind 做搜索吧，也许是哪里配置有问题？</p>
<p></p><p></p>
<p>其实 Pagefind 和 11ty 本身的关系并不大：Pagefind 是一个静态站点索引生成和搜索工具，它并不和任何一个站点生成器绑定。这意味着你可以选用诸如 Hugo 或者 Hexo 等其他静态站点生成器，它们接入 Pagefind 的过程都基本一致。</p>
<p>安装 Pagefind:</p>
<pre><code class="hljs language-sh">npm i pagefind
</code></pre>
<p>编辑 <code class="inline">package.json</code>, 添加一个新的指令启动 Pagefind 的索引生成：</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"scripts"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">"build-index"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx pagefind --site _site"</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>Pagefind 是通过直接解析生成后的 HTML 文件来构建索引的，所以需要先把站点通过 11ty 构建出来，再运行 <code class="inline">npm run build-index</code> 构建页面索引。Pagefind 会将必要的 CSS, JS 和索引文件输出到 <code class="inline">_site/pagefind</code> 文件夹下。</p>
<p>如果你需要构建站点的时候一并构建 Pagefind 的索引，那么也可以把这个指令追加到 <code class="inline">build</code> 条目里：</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"scripts"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">"build"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx @11ty/eleventy &amp;&amp; npx pagefind --site _site"</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>编辑页面模板，告诉 Pagefind 正文内容在哪里：</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- 在正文的父容器中添加 data-pagefind-body 属性 --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">article</span> <span class="hljs-attr">data-pagefind-body</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- 这里是你的正文 --&gt;</span>  
  <span class="hljs-tag">&lt;/<span class="hljs-name">article</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<p>如果你的标题并不是用 <code class="inline">&lt;h1&gt;</code> 之类的传统标题，或者 <code class="inline">&lt;h1&gt;</code> 已经被用作站点大标题，而文章的标题是用其他元素表示的，也可以告诉 Pagefind 以给定的元素内容为基准：</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">data-pagefind-meta</span>=<span class="hljs-string">"title"</span>&gt;</span>Hello!<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
<span class="hljs-comment">&lt;!-- 将 h2 设置为文章标题 ^ --&gt;</span>
</code></pre>
<p>再新增一个页面接入 Pagefind 的搜索框：</p>
<pre><code class="hljs language-html"><span class="hljs-comment">&lt;!-- 下面这个样式表需要修改成你对应存放 Pagefind 样式表的地址 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/pagefind/pagefind-ui.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>&gt;</span>
<span class="hljs-comment">&lt;!-- 下面这个 div 会存放 Pagefind 的全部内容 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"search"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"search"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-comment">&lt;!-- 下面这个脚本的地址也要跟随你的具体配置一并修改 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/pagefind/pagefind-ui.js"</span> <span class="hljs-attr">onload</span>=<span class="hljs-string">"new PagefindUI({ element: '#search', showImages: false });"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-comment">&lt;!-- ^ 如果你改了 div 框框的 id, 也需要修改这里的名字 --&gt;</span>
</code></pre>
<p>这样就足够了，没有太多活动部件。当然，如果你还有自定义 UI 的需求，比如使用模态搜索框而不是用一个单独页面的话，则可以参考官方文档中的 <a href="https://pagefind.app/docs/search-ui/">UI 自定义</a>相关的内容；以及，如果你还有更细化的需求，比如设置过滤、排序或者调整不同内容的权重，则也可以参考<a href="https://pagefind.app/docs/indexing/">官方文档</a></p>
<p>捋了一圈下来，没发现什么问题。不过扫了一眼 GitHub, 发现 Pagefind 在 1.5.0 中修改了<a href="https://github.com/Pagefind/Pagefind/issues/802">标题不参与搜索</a>的重要问题，更新包之后重建索引，问题解决。</p>
</body></html>]]></description>
<pubDate>Thu, 11 Jun 2026 14:21:52 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    
        
<item>
<title>直面死亡</title>
<link>/archives/facing-death.html</link>
<description><![CDATA[<html><head></head><body><p>不要温和地走进那个良夜。</p>
<p></p><p></p>
<p>谨以此文纪念曾真实地活过的人们。</p>
<p>我想，我们得谈谈。这种事情，我们最终是得谈谈的。</p>
<p>我们的教育，或者说基础教育阶段，对于人生中的与个人相关的大问题阐述得仍然是有所欠缺。与个人相关的大问题无非是两大类：生和死。今天，我们谈谈后者。</p>
<p>2021-07-15, 一位同学不幸离开了人世。具体的细节大家并不清楚——那时已经是暑假，大四的绝大部分人都已经毕业离校，留校的大多是需要补考或者延毕的，于是他所在的宿舍中只有他一人。清扫宿舍的工作人员比同一栋楼里的学生更先发现了这件事。但无论怎样，生命的逝去固然令人惋惜，再反复议论也无济于事。</p>
<p>生命是坚强而脆弱的，它只能被由内而外地打破。</p>
<p>于我而言，死亡是最终备选方案。目标的达成需要手段，但有些目标并不是那么容易达成；或者干脆就不能达成。不能达成却又要去达成，这是怎样的一种矛盾！矛盾的解决，有人会选择「换个梦不就得了」；有的人会作罢，从此一蹶不振；但还有的人会选择 Crash Fast, Crash Hard.</p>
<p>我们没有理由去嘲笑选择最终备选方案的人，就像我们没有理由去嘲笑知难而退和一蹶不振的人一样。对于一些目标近乎病态地追求，是获取成功的要素，也是通往疯狂的道路。这一线之隔、一念之差，通向两种完全不同的结局。</p>
<p>孤独可以毁灭人么？我认为是的。万念俱灰时无人安慰，是一种极致的痛苦。它或许并不如踢到了桌腿那么切实地疼，但是难受是确实有生理上的反应的。这种感觉就像陷入了深海之中被淹溺，它是一种长期的挣扎、疲惫和痛苦。</p>
<p>我们最终必须承认：人与人的情感并不相通。就像有人会觉得这些痛苦只是臆造的，只是因为这个人「矫情」才有这么多婆婆妈妈的事情。这种强烈的冲突和撕裂，如果只是发生在无关的人之间，或许还不算什么。但如果发生在朋友甚至亲人之间时，这便是致命的。很早之前我就发过关于自救互救与自杀干预的说说。自救互救现在，至少在深圳，已经好了很多；但后者，国内这一块做得……欠佳。干预的第一防线是身边的人，其次才是专业心理疏导和社会组织。当我们连身边的人都不再信任的时候，很难说向专业医师求助或者向社会组织求助有什么用。</p>
<p>我必须在这里再次强调一遍：干预的第一防线是身边的人，是亲人，是朋友。</p>
<p>真正要离去的人，出门时是不会发出什么响声的。如果那位同学只是有所诉求，我想事情并不会发展到这样一个地步。毕竟，九成尝试自杀的人，都只是有所诉求但穷尽了所有实现的手段，不得已选择了下下策。</p>
<hr>
<p>作为旁观者，这样简单的话很好说；但作为亲历者，目睹亲人的离去，则是另一回事。</p>
<p>很多时候我们并不是直面简单而冰冷的死亡，而是痛苦而漫长的割离。而这凌迟的开始，也并不会给人任何准备。这种事情对于整个家庭而言相当于一颗核弹毫无征兆地被扔在了每个人的头上——初始的伤害猛烈，而随后到来的放射性尘埃弥久不散更为棘手。</p>
<p>2023-09-12, 一场车祸打断了一个家庭的运行，也打断了我的日常生活。</p>
<p>病人入院时已经双目瞳孔散大，严重颅骨骨折、锁骨骨折、肋骨骨折。颅内出血已经将整个大脑的形态挤压到无法分辨半球和脑干。于是取下右侧颅骨清除颅内出血，但颅内压还是把右半球从洞里挤出了一点点——正常人的大脑距离颅骨可是有一定距离的。</p>
<p>但能做决定的那个人，无论是出于救母的心态也好，家庭内部的抗争也罢，决定坚持救治。</p>
<p>手术很干净，也很顺利。刚下台的人，除了浑身插满管子贴着电线以外，看起来和睡着了一样。「梦乡不过是死神温婉暂临」，在这个时候，又多了一层阴郁的色彩。</p>
<p>有些时候我们其实心里都清楚，一个人最好的结局应该是毫无痛苦地离去，而不是在这样一个半死不活的状态吊着。但是没人会真的这么去说，或者这么去做，毕竟「好死不如赖活着」。「放弃一切治疗」这种纯粹到冷血的方案，或许对于每个仍然存活的人都有利，但我们不能奢求任何人在任何时候都理性。</p>
<p>但理性的清算不会等感性退潮之后再进行。医疗费用，保险赔偿，和车主的索赔，救助基金的申请，一样一样，一件一件，都需要开始进行。垫付的费用，一天天地往上涨。我不知道这个本就支离破碎的家庭有没有哪怕一分钱来应付这样的事情——别骗自己了，自己掏了多少钱，账目上一清二楚，只是你的账簿没有向其他人公开罢了。</p>
<p>术后迎面而来的是水肿。不仅仅是手术区域的水肿，而是全身都会肿。这种超脱想象的病变，把人像一个气球一样吹了起来。她的面部竟然可以变得这样可怕而可憎，仿佛稍微一碰，整张脸就会破溃、流出混杂着脓血的组织液。</p>
<p>ICU 的味道很特别。如果说医院的味道是扑鼻而来的消毒水，那么 ICU 的味道是在这消毒水之上混叠了一层死亡的气息。是的，将死之人会散发出一股非常特殊的味道，那或许是体内的器官在一个个衰竭之后给世界留下的最后的警告。不久之后，一群人来到 ICU 门口，其中一名女士痛哭起来，抽泣了五分钟后，整理好情绪；然后，ICU 内门打开，一个平车上躺着一个老人被推了出来，人们围了上去，平车推了过去，消失在走廊尽头。</p>
<p>可怕的味道味道散了一些。</p>
<p>我不知道这股味道何时会从她的身上散发出来。</p>
<p>每天两支人血白蛋白，将近八百元。这白蛋白输了将近两个星期。不过，她也是从双侧瞳孔散大逐渐恢复，虽然两侧还是不等大。对刺激反射也在逐步恢复。虽然人可能醒不过来，但是命或许是保住了。水肿逐渐消退，她也回到了熟悉的模样——但是挂着一个噗嗤噗嗤的呼吸机。</p>
<p>「那么，白蛋白就可以先停了。」ICU 的医生嘱咐到：「不过病人有发烧，我们怀疑是肺部感染需要控制。」</p>
<p>如果一切都顺利，那么接下来就是转普通护理。医院会出具伤残鉴定，交警的定责已经确定了司机全责，那么之后保险赔付，一切结束。或许奇迹发生，她能醒过来；或许和 ICU 医生见过的几百个病例一样，她也会陷入永久的沉眠，谁也不知道。</p>
<p>他妈的，颅骨都没关上，怎么可能一周之内就可以转到普通护理开始康复治疗？CT 检查报告总是脑疝，我怎么可能会乐观？但人就是这么一种奇怪的生物。感性的抉择总是发生在理性的计算之前。</p>
<p>术后的并发症还是到来了。开颅的另一侧开始出血，月牙状的出血再次模糊了各个结构的边界。</p>
<p>其实这个时候，我才意识到，死亡大部分时间并不是一个干净利落的事情。即使从高楼坠落，即使孤身一人地死在阴冷的宿舍，死神的镰刀落下之后，仍然会血溅四方。</p>
<p>那时候我已经借故离开了这坨烂摊子，我以为这之后就是风平浪静。不过生活，生活不管你什么狗血还是巧合，生活会丢给你问题，然后让你自己解决。现在生活抛出的问题就是：你同意手术么？</p>
<p>手术，再做一次，或许下不来台；手术，放弃不做，那就可以准备办理出院手续了——哦不，因为你这个是交通事故导致的，所以你不能带她走。</p>
<p>远在天边的我并没有能力做决定。没有任何人能够代替那位独子做决定。在所有人心中都已经做好了理性的清算过后，他们都在等这拿着签字笔的手写下他们想要的答案。</p>
<p>他决定做手术。</p>
<p>这是一种悲情么？这是一种刻奇么？这是一种无奈的选择么？这是一种无声的抗争么？他的心里打着怎样的算盘？我不知道。</p>
<p>每个人都有每个人的故事。他的童年没有父亲的参与，而他的母亲，现在躺在 ICU 里的那位，也并不是一盏多么省油的灯。他呢，性格随他那远走高飞的父亲，办事随他这一生要强的母亲。没有稳定的工作，也更谈不上组一个新的家庭。三代人住在一间房——好在也就三个人。</p>
<p>他的母亲为这间房付出了一笔不菲的财产，所以长久地住在这里也是理所当然。但后来，她官司缠身，向我们三番五次地索要救济。我的父亲没有说一个「不」字。我的母亲为此也埋怨不少，但钱终究还是落到了对方手里。在那个月工资几百，一两千就已经是很大数字的年代，我们向她支援了好几个千开头的数额。</p>
<p>但这最终是一本糊涂账。如果细查流水，或许我们也早已「偿还」当初那套房她所出的份额，或许我们仍旧欠那么一些。这套房的归属，老人有她的打算，而她之下的子女们也各个有所觊觎。家长里短，不过如此。</p>
<p>漫长的等待之后，手术宣告成功。手术本身是成功的，但病人的预后比以前更为不乐观。之前我们或许还能期待奇迹发生，但现在我们就只能接受「最好的情况是植物人」这样一种结局。如果质问我的内心，我以为她压根挺不过这场手术，我甚至开始期待她挺不过这场手术，但我的良知并不允许我这么认为。</p>
<p>我现在甚至不知道医院有什么主意。也许医院方面只是慈悲为怀，不愿意任凭生命如此流逝。但如果我们以小人之心度君子之腹的话，难不成医院也已知道这人已经算是和我们阴阳两隔，现在的所有救治不过是为了再从这将死之人身上榨出一点钱财？</p>
<p>病危通知书下来了，但事情还未结束。</p>
<p>她一生要强的性格，以及 ICU 接连不断的工作，带她挺过了这三天。但和之前一样，手术再次延长了她逗留 ICU 的时间，也再次加重了这本就在温饱线上挣扎的家庭的负担——也加重了其他所有人的负担。</p>
<p>但似乎奇迹可以发生。在两周之后，她从 ICU 转出到了普通病房。虽然仍然在做着永恒的梦，但至少表面上乐观地来看，她有一天甚至可以离开医院，再次躺在她的那张床上。她或许无法回归到过去的生活，但于她身边周围的人而言，她仍然可以被视作家庭的一份子。</p>
<p>奇迹最终是没有能如愿到来。转到普通病房后不到两个月，并发症和感染再次袭来。她再次进入到了 ICU. 我对于她的印象停留在了那个看起来只是在沉睡的、脸上浮现着风霜但又平静安详的中年女性。我的记忆善良地帮我擦去了切开的气管和连接的呼吸机、拔掉了监护仪的连线，甚至病床和病褥也一并换成了洁白的絮。</p>
<p>2024-03-02 下午两点，伴随着全身性的脏器衰竭，她离开了这个世界。</p>
<p>命运为她的生命画上了句点。而我们只知道离开远不代表结束。接下来，是交警队的核查，是与保险公司的官司，是和肇事司机的周旋，是一场可能只有一个人在场的葬礼。我有幸而不幸地可以当一名冷眼的旁观者。我难以想象亲历者所需要经历的一切需要多大的勇气。</p>
<p>这个时候很难压抑心中「如果当初」的想法：如果当初，她没有选择冒着倾盆大雨出门；如果当初，她在过马路的时候可以等一下再走；如果当初，她没有选择过那个路口，而是选择另一条路线……甚至更残忍地说，如果当初救护车再晚一点到，也许心里这块肉就可以用利刀一下剁开，而不是用一根已经生锈的钝锯条，一点点地磨到只剩最后一丝希望，再一锤子用力敲下。</p>
<p>葬礼，并不是只有一个人到场。这或许是唯一的慰藉。2023-05-13 清晨，一行人出发为这一切写下最后的结语。在殡仪馆，我没有勇气去再仔细地看她一眼。法医的鉴定流程紧赶慢赶也进行了整整 45 天。入殓师只能很勉强地将已经彻底失水干瘪而支离破碎的脸庞撑起，让她还能留存一丝最后的体面。</p>
<p>不久后，她化为了一缕青烟，留下一捧白灰。我举着她的相片，来到一处公墓；他将小匣放下，做好最终的告别。这是一场平静而简单的葬礼，没有哀乐，也没有歇斯底里。一切的一切在中午到来之前归于沉寂。</p>
<p>亲人的离去也许并不仅仅是一场倾盆大雨，而还有浸没了余生的潮湿。</p>
<p>而我只担心我无法为即将到来的可能做好准备。</p>
<hr>
<p>但有些时候，死亡的降临也并非如此漫长。它有时会很乐意让一切在一瞬发生，在你最没有防备的时候，它会打你一个措手不及。</p>
<p>2024-11-15 晚 9 点，我突然接到家里的消息，告诉我姑父因脑溢血去世了。送到医院时人已经不行了，医生也无力回天。他们现在正在等待殡仪馆转送。</p>
<p>母亲告诉我，请择日给姑姑打个电话问候一下，然后匆匆挂断了电话。</p>
<p>说实在话，我没法面对这样的事情。打电话的话，我该说些什么呢？对此我毫无准备！我该说些什么？我该不该现在赶紧买票回去？就算我回去了，又怎么样呢？</p>
<p>这种冲击、这种不知道该做什么的感觉瞬间淹没了我的小房间。亲人的离世一个接一个，让我无法适从。我想不明白命运为何要如此地捉弄每一个人，难道这就是所谓的「时间到了」么？</p>
<p>这种手足无措持续到了周一。我想，无论怎样不能再拖了。这通电话必须要打，现在就要打。</p>
<p>晚上七点钟，我跑到公司的楼梯间，拨通了电话。</p>
<p>电话接通的时间如此漫长，一声、两声、三声……有一瞬间我甚至希望这通电话接不通，我可以懦弱地退出、我可以装作不知道这回事。我可以假装一切的一切都没有发生，世界仍然是按照它过往的那样运行着。</p>
<p>不，不能退缩。我已经做好了面对沉沦之人的准备，我会学着成为一个大人，我会帮助在低谷的人重回到阳光之下。</p>
<p>但电话接通的那一刻，却是她先呼唤了我的名字。我和她寒暄了两句，委婉地告诉了她我已经获知了这份消息；她却反过来问我，刚下班吧，赶紧去吃饭吧。</p>
<p>她那么坚强，我感觉我才是被安慰的那个。</p>
<p>挂断电话之后，我并没有觉得轻松。我觉得「如鲠在喉」居然是这么贴切的一个成语。这份自心底升起的悲伤和阴郁，卡在了脖子正中央。</p>
<p>他才六十多岁的年纪，就这样，在没有人犯错的情况下，消逝了。一个年纪和我父母相仿的亲人，就这样毫无征兆地离开了。</p>
<p>想要在情感的冲击下写作是非常不容易的。或许是因为寒冷，或许是因为激动，我打字的手颤抖不已。这可以说是我第一次觉得深渊向我回望了：一个人可以在不因任何过错的情况下，被如此轻易地抹去。死亡从这一天起，不再是一个遥远的课题，而是一朵笼罩半空的乌云。</p>
<p>我想起了费恩曼博士。</p>
<p>在他的自传中，提到了他妻子因肺结核的离世。费恩曼博士也是一位坚强的人，身处曼哈顿计划中的他，接到噩耗之时，简单整理了情绪便继续了手头上的工作。自葬礼回到基地的他，面对同事的关心，也并没有因此而情绪失控，只是简单地回应了几句，便立刻询问计划的进展。</p>
<p>但一年之后，费恩曼博士在橱窗前看到了一袭裙子，想到或许他的妻子会喜欢之时，压抑了多年的情感便在此时重新鲜活。博士潸然泪下。</p>
<p>我不愿想象同样的场景发生在我的亲人的身上，但这或许于所有人而言，都是最终的答案。</p>
<hr>
<p>死亡有些时候，似乎也会听从社会的差遣，给所有人一些准备和预期，以至于它的降临甚至不再是一种意外。</p>
<p>2025-04-23 中午，父亲的一个电话打破了平静。奶奶意外摔断了髋骨，刚从医院回来，赶紧回来看一趟吧。</p>
<p>每个人都知道老年人意外复杂骨折意味着什么，这回应该是最后一面了。刚从医院回来自然是因为医生也没啥好办法治，这个岁数的人压根不可能说什么骨折了打个钢钉修修，上了手术台之后能不能下来都两说。</p>
<p>我急匆匆地买了当晚回家的机票。老人的精神状态倒是还行，不过依从性很差。短暂见面交谈几句之后，我便因工作原因又回到了深圳。</p>
<p>不久之后，老人就开始出现谵妄了——抗拒帮助、喜怒无常，以及开始提起见到了已逝的其他亲人。父亲那辈人紧急开始修缮老家废弃已久的屋子，为的是老人最终能有个归宿。</p>
<p>2025-06-05 中午，再次接到父亲的电话，我已经能猜到发生了什么事情。对于老人来说，这可能是相对而言最体面的离开了。我又再次火急火燎地买了机票回家。</p>
<p>或许是因为所有人都已经做好准备，或许是因为精神疲乏，一路上我却没有什么感觉。我只觉得结束了，终于结束了。悬在头顶的那柄达摩克斯之剑，终于砍下来了。疼么？没关系，人头落地了，已经不会疼了。</p>
<p>按照习俗，要守灵。大家默默地坐在院子里，没什么太多交流。空气中只有无尽的闷热和给你强打精神的蚊虫。</p>
<p>我感到害怕，但并不是怕死亡无声的降临，而是怕当别人问起「什么是按当地习俗办」时，我无法答出个所以然。我只能木然地观察着周围的人忙里忙外。我不能理解为什么他们要做这些事情，没有人和我说过需要做这些事情，也没有人解释为什么这样做。这种流程、这种说辞、这种哭嚎、这种行进、这种不真实感、这种解离，让我觉得我只是做了一个无彩排的「表演」，好像其实生活还是原来的生活，谁都没有离去。</p>
<p>叶落归根，我望着那个新垒出来的坟包，只觉得在这田地里又多了一分突兀。可能这就是「当地习俗」的最终体现吧：人从一捧黄土中来，回到一捧黄土中去，中间往往只是过场。</p>
<p>现在，我坐在电脑前，仍然没法「正确」地整理出一个情绪。我不知道为什么，明明是至亲之人，她的离去却没有在我的生活里激起任何波澜。加缪在写《局外人》的时候，脑海里构想的会是这样的一个场景么？</p>
<p>这是曾带过我长大的，我的奶奶的死亡。</p>
<p>这或许也是我的死亡。</p>
<hr>
<p>人生大事，无非生死。但是我们似乎未曾为这两件大事做过任何准备，而只是等待它们毫无征兆的降临。</p>
</body></html>]]></description>
<pubDate>Wed, 10 Jun 2026 17:29:09 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
</item>

    </channel>
</rss>
