<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[喵内]]></title><description><![CDATA[楼外的蒹葭、傍晚的月亮和鸡鸣寺的樱花。]]></description><link>https://blog.caiths.com</link><image><url>https://pic.imgdb.cn/item/653e1b74c458853aef64d366.jpg</url><title>喵内</title><link>https://blog.caiths.com</link></image><generator>Yohaku (https://github.com/Innei/Yohaku)</generator><lastBuildDate>Wed, 19 Aug 2026 14:09:18 GMT</lastBuildDate><atom:link href="https://blog.caiths.com/feed" rel="self" type="application/rss+xml"/><pubDate>Wed, 19 Aug 2026 14:09:18 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[一份测线规划基准能回答到哪里]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/tech/planning-baseline-boundaries">https://blog.caiths.com/posts/tech/planning-baseline-boundaries</a></blockquote><div><p>2026 年 4 月 30 日，我建立了一个公开的多波束测线规划基准仓库，同日发布 <code>v0.1.0</code>。代码、派生的 CSV/JSON、图表、环境文件和数据引用从这一天起可以对外查到。这个日期只对应数值规划包的公开节点，不对应出航、海上试验或测量验收。</p><p>重新整理这份仓库时，我先把这几个概念拆开。它处理的是公开海底地形先验上的规划问题：比较固定航向测线的方向、非均匀间距、路径长度、预测覆盖、过度重叠和局部修复。船体姿态、声速剖面、导航控制、波束级声学和原始声呐产品都不在当前证据里。规划结果可以重算，测量结果还没有发生。</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<h2 id="">公开包保存的是计算链</h2><p>基准使用 GEBCO 2025 Grid 和 USGS Southern Cascadia 30 m 复合海底地形产品，仓库为两者保留了 DOI。体量较大的原始数据没有提交；需要重跑导入时，仍要回到官方 DOI 页面取得对应产品。仓库里留下的是小型处理缓存、派生表格和分析入口。</p><p>这个安排牺牲了“克隆以后立即拥有全部数据”的便利，却把归属关系保留下来：一张图或一个覆盖数字来自哪项公开数据、经过哪个入口生成、写入哪份表格。上游产品如果更新，旧结果不会因此自动变成新结果，重跑者也不能绕过数据版本重新解释它。</p><p><code>environment.yml</code> 记录了 Python、NumPy、Pandas、Matplotlib、Rasterio 等环境项。当前代码锚点也包含 <code>check_release_readiness.py</code>：它在 5 月 23 日首次加入，并在 7 月 12 日的 <code>a51dcf1d</code> 中更新，用来检查当前公开树是否缺少必需文件和目录，以及是否混入禁止路径或禁止词。这个脚本不在 4 月 <code>v0.1.0</code> 的标签树中，也不负责数值重跑；它只说明后来的仓库结构增加了一层发布前检查。</p><p>所以我没有用 7 月的整理结果反向装饰 4 月的 Release。前者说明当前树怎样约束文件和声明，后者保存当时公开的代码与派生产物。对这份基准而言，“可重放”至少要能回答数据从哪里来、脚本从哪里运行、输出落在哪里；它没有承诺所有机器的耗时一致，也没有把存档包写成海测产品。</p><h2 id="">一条得分更高的测线仍可能失败</h2><p>在公开网格上，路径长度、预测覆盖率和过度重叠可以形成几项直观指标。问题是，这些汇总值会压掉空间位置。更短的线路可能丢掉局部覆盖，平均重叠平稳的线路也可能在峡谷壁或坡度变化处留下集中缺口。</p><p>这也是仓库没有只保存一张最终路径图的原因。网格分辨率、先验误差、覆盖阈值、最大未覆盖块、局部单元重叠和修复决策被拆成不同诊断。优化器回答“在当前假设下哪条线路得分更高”，这些诊断继续追问“假设变动以后，它会从哪里失效”。两类问题需要同时留下。</p><p>粗先验到细网格的回放给出了最直接的一次提醒。诊断在 <code>300x240</code> 的细网格上，回放用 120、300、600 和 900 米先验网格选出的布局，并为混合方法使用 20 个种子。在高复杂度裁剪上，混合方法的回放可行率从 120 米时的 <code>1.00</code> 降到 900 米时的 <code>0.35</code>；低、中复杂度场景在同一组表里更稳定。</p><p>单看这组数字，不能得出“900 米先验普遍不可用”的结论。地形复杂度必须留在表里：只报告四种尺度的总体印象，会掩盖高复杂度场景先失效的事实。先验越粗，可能减少栅格计算量，也可能丢掉局部结构；这两件事必须放在同一张判断表里。</p><p>结构化先验误差又增加了三种更具体的偏差：相关低频偏差、坡度放大偏差和局部峡谷壁偏差，每个场景仍使用 20 个种子。两个 GEBCO 场景与 USGS 高复杂度场景没有给出同一种响应，后者更容易暴露固定间距方案的过度重叠与可行性问题。这些公开网格扰动只是规划层压测，不能被改称为任务日志或部署验证。</p><h2 id="">流偏移代理只是一种比较条件</h2><p>流偏移回放把流速和方向映射成残余横向偏移、航向扰动、低频足迹变化，以及与地形耦合的足迹缩小。相同场景和扰动下的不同方法使用共同随机数，因此比较落在同一组扰动样本上。它适合观察固定测线几何在代理扰动中的相对差异，不构成流体动力学模型。</p><p>执行风险修复再把部分流偏移、航向和足迹风险从事后回放移入候选布局的选择。我后来才把这件事想清楚：已知的失效条件不必等到最终图表里才出现，也可以提前成为拒绝候选的理由。但目标函数仍然缺少反馈控制、流体动力学、声速不确定性和任务日志。它只让数值目标更难通过，尚不能证明线路已经更容易执行。</p><h2 id="">二元通过不能替代局部检查</h2><p>在默认平均门禁之外，阈值与局部失效诊断继续检查 95%、97%、98%、99% 和 99.5% 覆盖阈值，以及 1%、2%、3% 和 5% 平均过度重叠门禁。在更严格的 <code>C99/O2</code> 条件下，GEBCO 的混合布局并非每次都被接受。最大未覆盖块和 p99 单元过度重叠还会标出均值没有呈现的位置。</p><p>分段航向也没有因为自由度更多就自动进入结果。公开决策审计里，有的场景需要块级修复，有的场景在覆盖门禁处直接拒绝分段方案，还有的场景虽然得到覆盖改善，却因转换代价继续保留单一航向。这份审计重算了决策逻辑，没有重跑规划器，也没有实现 Dubins 路径或 AUV 控制。拒绝分段同样是一项结果，因为它保留了拒绝发生在哪个条件上。</p><p>足迹有效性检查提供了另一种容易被误读的“通过”。在 9 个代表性场景-方法布局里，将总宽度代理改为分别保留左舷与右舷足迹范围后，<code>C97/O3</code> 可行性判定没有变化；与此同时，USGS 高复杂度场景的局部单元计数最大分歧达到 <code>10.42%</code>。</p><p>如果只保留二元可行性，这项检查会被压缩成“零翻转”。局部计数分歧说明两个足迹表示并不等价。当前证据支持规划层的门禁解释没有被翻转，无法支持足迹代理已经代替波束级声学、姿态和声速验证。</p><h2 id="-release-">四月的 Release 与七月的代码锚点</h2><p>2026 年 7 月 12 日，公开主分支固定在提交 <code>a51dcf1d5166dd798007dfd72a6b7ff30530c11b</code>。这次提交将仓库元数据与当前稿件对齐，README 也说明仓库名是历史名称，当前支持的贡献是固定测线的 MBES 规划基准。本文以这个提交为代码锚点，没有从本地未提交文件取材。</p><p>四月的标签已经有粗先验的早期版本（120、300、600 米，混合方法使用 0--4 号种子）。后来补进的是 900 米与 20 种子的扩展，以及结构化误差、执行风险和局部门禁；这些都不能被算进 <code>v0.1.0</code> 当时已经完成的内容。版本分开以后，仓库的变化才可以被复查：早期公开了什么，后来补了什么，哪些声明至今仍为空白。</p><p>整理这组结果以后，我更在意一条线路在什么条件下应被拒绝。最短路径、较高覆盖均值或一次二元通过都不足以替代局部缺口、先验敏感性和执行代理的检查。当前公开包已经能暴露一部分不稳定分支，下一层证据仍需要控制器执行、任务日志、声速不确定性和原始 MBES 产品级核查；这份仓库因此停在出航以前，保存的是规划问题怎样被计算、怎样被压测，以及哪些结论还不能写成测量结果。</p><h2 id="">资料与版本</h2><ul><li>当前代码锚点：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/commit/a51dcf1d5166dd798007dfd72a6b7ff30530c11b">提交 <code>a51dcf1d</code></a> 与该提交下的 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/README.md">README</a>。</li><li>4 月公开节点：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/releases/tag/v0.1.0">Release <code>v0.1.0</code></a>，其标签指向提交 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/commit/625fb6a371b0bc2c2bee70e190024e4a418907d9"><code>625fb6a</code></a>。</li><li>先验回放：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/coarse_prior_replay/README.md">粗先验到细网格</a> 与 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/structured_prior_error_replay/README.md">结构化先验误差</a>。</li><li>执行扰动：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/current_drift_replay/README.md">流偏移回放</a> 与 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/execution_risk_refinement/README.md">执行风险修复</a>。</li><li>局部门禁：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/threshold_local_failure_extension/README.md">阈值与局部失效</a>、<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/segmented_decision_audit/README.md">分段决策</a> 与 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/footprint_validity_audit/README.md">足迹有效性</a>。</li><li>环境与仓库卫生：<a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/environment.yml">environment.yml</a> 与当前的 <a href="https://github.com/poboll/geo-auv-bathymetry-benchmark/blob/a51dcf1d5166dd798007dfd72a6b7ff30530c11b/check_release_readiness.py">发布准备检查</a>（5 月首次加入，7 月更新）。</li></ul></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/tech/planning-baseline-boundaries#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/tech/planning-baseline-boundaries</link><guid isPermaLink="true">https://blog.caiths.com/posts/tech/planning-baseline-boundaries</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Mon, 10 Aug 2026 01:22:52 GMT</pubDate></item><item><title><![CDATA[整理素材时，先决定哪些不公开]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/notes/34">https://blog.caiths.com/notes/34</a></blockquote><div><p><em>整理于 2026-08-09。这个日期只表示我在今天完成了复核，并不对应文中素材的发生时间。</em></p><p>今天把待整理的材料重新过了一遍。我没有先挑选题，也没有计算还能拼出几篇文章，先删掉了一批不该公开的内容。它们未必写得差，只是离开原来的语境之后，已经不适合出现在一个任何人都能打开的页面上。</p><p>有些记录碰到了别人的生活。即使删掉名字，前后细节也可能让人认出来。另一些只写下了当时的计划，后来却被整理成已经完成的结果。还有些句子只剩下判断，找不到可以复核的依据。为了让文章顺一点，再补一段场景、一个转折或一组数字，文字会变完整，事情却会走样。这些材料停在草稿里更合适。</p><p>能留下的部分也不必都写成长文。短促的念头放在手记里就够了；项目记录只保留能够核对的进展，没跑通的地方照样写清楚；旧稿里那些太响的标题和提前准备好的结论，可以直接删掉。整理旧内容不要求救回每一页。有些文字只是服务于当时的记忆，留在原处也算一种归档。</p><p>公开会改变一段记录的边界。原本只供自己回看的话，一旦放到网页上，就要考虑别人的信息会不会被带出来，计划会不会被误读为成果，几段不同时间的材料会不会被拼成一条从未发生过的时间线。篇幅和文气解决不了这些问题，只能在发布以前逐条判断。</p><p>这一轮复核最后留下的是一份更短的待发布清单。没有公开的内容并没有消失，只是留在更合适的位置。能进入博客的部分，也有了更清楚的来路。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/notes/34#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/notes/34</link><guid isPermaLink="true">https://blog.caiths.com/notes/34</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Sun, 09 Aug 2026 06:03:04 GMT</pubDate></item><item><title><![CDATA[RSS 返回 500 的那几天]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/thoughts/lkurococ-b5814d78">https://blog.caiths.com/posts/thoughts/lkurococ-b5814d78</a></blockquote><div><p>8 月 2 日，博客页面照常打开，文章也都还在，三个订阅地址却一起返回了 500。</p><p>我一开始盯着前端 route 看。生成 XML 的代码不长，看起来很像那种改完一处异常、重新部署就能结束的小故障。后端负责提供订阅条目的接口又是 200，里面有 10 条公开内容，更让人觉得问题不会走得太远。</p><p>后来几天里，这个 500 先后经过了前端依赖、主题配置、上游同步和边缘缓存。每一处单独看都不复杂，放在一起却很容易互相遮住。现在线路已经恢复。回头看，单独记住某一行修复意义不大；那些一度同时成立、却看起来彼此冲突的结果，更值得写进复盘。</p><h2 id="8--2-">8 月 2 日，三个地址一起报错</h2><p>当时 <code>/feed</code>、<code>/feed.xml</code> 和 <code>/atom.xml</code> 都返回 500。给 RSS 提供文章条目的后端接口正常，另一个 aggregate 接口却因为没有公开手记，在读取最新手记时返回了 <code>NOT_FOUND</code>。</p><p>旧 route 用同一个 <code>Promise.all</code> 等待这两个请求。feed 提供订阅条目，aggregate 只是补充站点标题、描述和主题配置；代码却让它们拥有了相同的决定权。aggregate 一旦失败，已经取回的 10 条内容也来不及生成 XML。</p><p>前几次排查里，我记过缓存、主题开关、内容数量和重新部署等猜测。它们都能解释一部分表象，却解释不了“后端条目稳定返回 200，三个前端入口同时为 500”。直到两个请求结果被并排放在一起，问题才缩到这段不合适的等待关系上。</p><p>第一版修复将 feed 保留为必需数据。feed 失败时，route 不伪造空订阅；aggregate 失败时，标题和描述退回 feed 已有的元数据，主题配置走受约束的默认值。聚焦测试覆盖了有主题、旧主题缺字段、没有主题、aggregate 请求失败和 SEO 字段为空等情况。那一版有 6 个测试，构建和生产冷请求也通过了。</p><p>我以为事情到这里已经结束。两天后，后台又给出了另一份答案。</p><h2 id="8--4-">8 月 4 日，后台还缺一项</h2><p>继续检查生产配置时，主题 snippet 里没有 RSS 模块。前端 fallback 能让旧配置继续工作，后台却没有明确保存站点是否输出全文订阅。</p><p>这次改动没有进入文章表，也没有直接操作数据库。主题配置通过原有接口补入官方默认值：自定义元素为空，<code>noRSS=false</code>。写入前保存原值，写入后再读回来；移除新增的 RSS 字段以后，其余内容应与原值一致。公开 aggregate 随后也读到了相同语义，只是字段名被序列化为 <code>no_rss=false</code>。</p><p>代码里的默认值和后台保存的选择看起来相同，承担的事情却不同。前者让旧配置或临时请求失败时仍能生成结果，后者说明这个站点此刻明确允许什么。只看到页面恢复，很容易漏掉这项差别。</p><p>当时我差点将故障归档成“后台少了一个开关”。如果记录停在这里，8 月 8 日再看仓库时会更难解释。</p><h2 id="">线上没坏，源码却退回去了</h2><p>8 月 8 日检查主分支，前一版 route 容错已经消失。每天同步上游的任务会用上游目录树替换当前工作树，然后只取回少量下游文件。RSS helper、测试和 route 修改没有全部进入保留范围，一次正常同步便覆盖了补丁。</p><p>生产当时仍然健康。后台配置已经补齐，公开域名也继续指向上一份成功部署。只看 <code>/feed</code>，我会得到“一切正常”的结论；等下一次部署，旧行为才可能重新上线。</p><p>后来的 <a href="https://github.com/poboll/Shiroi/pull/10">PR #10</a> 同时处理运行代码和同步流程。feed 继续提供必需内容，aggregate 改为可选请求。主题存在但缺少 RSS 字段时使用公开默认值；aggregate 整体不可用、无法确认全文偏好时，则按 <code>noRSS=true</code> 处理：订阅入口和原文链接保留，正文不直接输出。</p><p>同步任务也开始保留下游 helper、测试和工作流，并用精确锚点将少量 route 变化重放到最新上游代码上。锚点失效时任务会明确失败，不再安静地覆盖补丁。聚焦测试增加到 7 个，其中一个让真实 rejected Promise 进入配置解析，专门验证 aggregate 失败时的关闭策略。</p><p>PR 合并后的提交是 <code>06991c0e5f7246372a3eb0bcbf05f727870b98c5</code>。部署完成后，我重新走了一遍公开入口。</p><h2 id="">冷请求比刷新页面更可信</h2><p>后来验收不再只看 <code>/feed</code> 的一个 200。两个域名分别请求 <code>/feed</code>、<code>/feed.xml</code>、<code>/atom.xml</code>、<code>/thinking/feed</code> 和 <code>/says/feed</code>，每个地址都带上唯一参数，避开已有的边缘缓存。</p><p>十个响应都是 <code>200 application/xml</code>，XML 根节点均为 <code>rss</code>。每个域名的五个入口，条目数依次为 <code>10/10/10/17/20</code>。缓存结果为 <code>MISS</code>、<code>age=0</code>，没有出现浏览器挑战。主 feed 的 GUID、日期、HTTPS 链接和自动发现标签也分别检查，条目链接能够打开。</p><p><code>/atom.xml</code> 仍只是兼容地址。它返回的根节点是 <code>rss</code>，并没有因为路径名里带着 atom 就变成 Atom 格式。这条地址继续留在回归检查里，只为保护已经存在的订阅，不替它补一个当前没有的能力。</p><p>冷请求也解决了另一个误会。订阅结果可能被边缘缓存保留，一次普通刷新命中的有可能仍是旧版本。<code>MISS</code>、XML 解析和条目数量放到一起，才说明眼前这份部署真的从后端取回了内容并生成结果。</p><h2 id="429--rss-">429 没有和 RSS 一起收尾</h2><p>同一合并提交的生产部署和 RSS 冷验都通过，主分支的 Bundle Analysis 却失败了。静态生成进行到大约 42/85 个页面时，多语言页面集中请求 aggregate，后端开始返回 <code>429 RATE_LIMITED</code>。日志里同一接口先出现过 200，随后才进入限流。</p><p>这条失败没有被并进 RSS 故障。回退 route 容错不会减少静态构建的请求量，重复修改后台开关也帮不上忙。它需要另一组改动：控制构建并发、请求去重，或者为 aggregate 选择合适的缓存。</p><p>后来的 <a href="https://github.com/poboll/Shiroi/pull/11">PR #11</a> 才单独处理这条线。同一语言的 aggregate 读取开始共用缓存和单次在途请求，Vercel 构建也限制为一个预渲染 worker。冷构建最终生成 85/85 个页面，只发出 5 次 aggregate 请求，分别对应 5 种语言，响应全部为 200。</p><p>几天的记录摊开以后，可以看到几条各自独立的线：最初的 500 来自可选请求拖垮 route；后台确实缺少显式配置；同步任务又让已经通过测试的补丁从源码里消失；边缘缓存要求验收使用冷请求；构建 429 则在另一份补丁里收束。</p><p>我以前总想尽快找到唯一根因，再让其他异常都围着它解释。这一次，后端 200 和前端 500、线上健康和源码回退、生产部署成功和分析任务失败同时存在。承认它们属于不同阶段以后，排查反而变得简单了一些。</p><p>现在十个订阅入口都能解析，后台保存的意图和前端处理异常的方式也已经对上。同步锚点仍要跟着上游结构继续照看。记录停在这里，既没有把一个 500 写成灾难，也没有用后来通过的构建抹掉当时那次失败。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/thoughts/lkurococ-b5814d78#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/thoughts/lkurococ-b5814d78</link><guid isPermaLink="true">https://blog.caiths.com/posts/thoughts/lkurococ-b5814d78</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Sat, 08 Aug 2026 15:15:00 GMT</pubDate></item><item><title><![CDATA[自动化 Mac 之前先留一个停止按钮]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/notes/33">https://blog.caiths.com/notes/33</a></blockquote><div><p>把 Mac 上一件重复操作交给脚本之前，我会先问一个不太像效率问题的问题：它要怎样停下来？</p><p>一个小脚本可以打开应用、读取一份本地文件、整理内容，再把结果写到指定目录。真正麻烦的是应用弹出确认框、页面还没加载完、文件格式变了，或者脚本跑到一半时我已经不想继续。没有停止条件的自动化，省下的几分钟很容易变成一小时排查。</p><p>我现在会把流程拆成四段：输入、转换、输出和记录。输入只读，不在脚本里保存账号或 Token；转换阶段先写到临时目录；输出前检查文件数量、大小和哈希；记录里只保留时间、步骤和错误原因。任何一段失败，都不继续往下一段传递半成品。</p><p>如果需要控制图形界面，优先寻找应用提供的快捷键、文件导入或本地接口，最后才用坐标点击。坐标会随着窗口大小变化，页面也可能出现登录、更新或权限提示。脚本最好有 <code>--dry-run</code> 和超时，执行前告诉我将读取什么、写到哪里，执行后能用一个退出码说明结果。</p><p>自动化也不等于把 Mac 变成没人照看的机器。它应该把重复动作变成一条可以暂停、检查和恢复的流程。先留一个停止按钮，再谈它能替我点多少次。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/notes/33#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/notes/33</link><guid isPermaLink="true">https://blog.caiths.com/notes/33</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Mon, 03 Aug 2026 17:07:31 GMT</pubDate></item><item><title><![CDATA[把红外控制拆成 AP、状态和协议]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/tech/esp8266-ir-controller">https://blog.caiths.com/posts/tech/esp8266-ir-controller</a></blockquote><div><p>重新读这份 ESP8266 固件时，我先改掉了旧稿里一句看起来很顺的话：页面拿到 <code>200</code>，不等于空调确认了状态。代码没有提供这样的回执。</p><p>2025 年 7 月 15 日的<a href="https://github.com/poboll/esp-media-ir/commit/ee3c2463f8cc3dfb678185732d9e17d628ab92f0">固定提交</a>里，控制链路很短。ESP8266 自己开一个 Wi-Fi 热点，浏览器提交 HTTP 请求，处理函数更新几项本地变量，再调用红外发送函数。源码能说明调用顺序，不能让各层结果互相作证。</p><h2 id="">请求在代码里怎样走向发送函数</h2><p>固件使用 AP 模式，地址固定为 <code>192.168.4.1</code>。<code>DNSServer</code> 将连入热点后的域名请求指向本地页面，<code>ESP8266WebServer</code> 提供页面与控制路由。<code>loop()</code> 里只处理 Web 客户端和下一条 DNS 请求，没有云端账号，也没有 MQTT 或家庭路由器配网。</p><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p>
<p>页面上的开关、温度、模式、风速和睡眠按钮分别请求 <code>/power</code>、<code>/temp_up</code>、<code>/temp_down</code>、<code>/mode</code>、<code>/speed</code> 与 <code>/sleep</code>。处理函数通常先修改变量，再调用 <code>sendMideaCode()</code> 或关机发送函数，最后把 <code>buildStateString()</code> 的结果放进 HTTP 响应。</p><p>从源码顺序只能确认：正常处理路径在返回响应前包含发送函数调用，响应内容取自固件记录的状态。仅凭源码，无法知道一次真实请求是否走到该处，也无法确认红外发射管是否正确接线、脉冲是否从硬件发出，或空调是否识别并执行了这组时序。空调到开发板之间没有反向数据通道。</p><h2 id="">页面显示的是本地记录</h2><p><code>buildStateString()</code> 把开关、温度索引、模式索引、风速索引和睡眠标记拼成一串文本。浏览器收到文本后更新界面，所以屏幕展示的是 ESP8266 当前记住的设置。</p><p>这份记录有用。它能说明某次请求改变了哪几个变量，也让排查可以沿着浏览器、路由、状态、编码和发送函数逐段进行。但它不是空调的读数。有人使用实体遥控器改了温度，开发板并不知道，页面仍会停在原来的值。</p><p><code>/timer</code> 更能说明两者的差别。这个路由连接的是 <code>handleNotImplemented()</code>，函数没有实现定时动作，却照样返回 <code>200</code> 和当前状态。这个反例把三层结果分开：HTTP 是否返回、固件是否调用发送函数、目标设备是否实际响应。三项不能互相代替。</p><h2 id="">三个字节怎样变成两帧时序</h2><p>仓库把美的协议整理成 A、B、C 三个字节。A 使用固定用户码 <code>0xB2</code>；B 从风速表中选择，睡眠模式则暂时使用单独的字节；C 由温度表和模式表按位与得到。关机没有复用普通组合，而是使用 <code>0xB2, 0x7B, 0xE0</code> 这组负载。</p><p><code>generateMideaCode()</code> 会依次编码字节及其反码，生成两帧原始时序。<code>sendMideaCode()</code> 再调用 <code>sendRaw(..., 38)</code>。这里的 <code>38</code> 是代码选择的载波频率参数，双帧也是固定实现的一部分。文章能确认的是这些数组、运算和函数调用，不能由此补出目标空调上的成功率。</p><p>睡眠字节 <code>0xCE</code> 在源码注释里带着一条警告：它是占位值，README 要求用实体遥控器重新捕获真实指令后替换。在捕获和复测完成前，页面上的“睡眠”按钮不能证明睡眠模式已经可用。</p><h2 id="">代码里还有几个空位</h2><p>当前路线图里的定时、MQTT、断电状态保存和更多品牌型号都没有完成。仓库存在 GitHub Pages workflow，但 CNAME 仍是示例域名；配置文件存在，也不能替代一次公开部署验收。</p><p>主循环、路由和协议表已经给出一条可以阅读的程序路径。固定材料里没有目标型号、供电与接线、各按钮的实机结果、睡眠码捕获，也没有空调无响应时的串口或红外接收证据。因此这里只能写“代码调用了发送函数”，不能写“设备已经完成控制”。</p><p>这次修订只把 <code>200</code> 放回它实际能证明的位置。实机材料仍然缺失，页面本地状态与空调动作是否一致暂不下结论。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/tech/esp8266-ir-controller#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/tech/esp8266-ir-controller</link><guid isPermaLink="true">https://blog.caiths.com/posts/tech/esp8266-ir-controller</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Mon, 03 Aug 2026 16:00:00 GMT</pubDate></item><item><title><![CDATA[一个博客系统的边界，从页面一直到回滚]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/projects/build-personal-blog">https://blog.caiths.com/posts/projects/build-personal-blog</a></blockquote><div><p>一个博客最开始只需要一个 Markdown 文件和一个静态托管。文章多起来以后，后台、图片、搜索、RSS、手记和评论逐渐加入，系统也从“把页面发出去”变成了一条需要长期照看的链路。</p><p>我现在会先把它画成几层。浏览器访问 Vercel 上的前端，前端通过公开 API 读取内容，API 由主机上的 Nginx 转给 MX Space，数据落在 PostgreSQL，缓存和搜索又有各自的更新时机。两台备用服务器站在旁边，平时不接流量，只有主机故障或维护窗口才参与切换。层次写清以后，看到 500 时不必立刻重启所有服务。</p><p>日常发文也应该沿着这条边界走。先在后台或 API 创建草稿，写完以后读回标题、正文、slug 和发布状态，再让首页、详情页、归档和 RSS 各读一次。直接改数据库虽然看起来快，却会跳过缓存失效、搜索索引和内容更新事件；数据库里的值正确，页面仍可能是旧的。</p><p>日期是另一种容易被忽略的字段。文章的创建时间决定归档顺序，手记还可能有独立的公开时间；只改 <code>publicAt</code> 不会修复列表里的同日堆叠。没有原始证据时，我宁愿把稿件留成私有，也不为了让时间线好看而假装它在某个日期发生过。</p><p>回滚要提前存在。每次批处理前保存原正文哈希、标题、摘要、日期和发布状态，写回后逐条读回；一条失败就按照相反顺序恢复已经尝试的条目。API 的 <code>modifiedAt</code> 可能无法回到旧值，但内容和公开状态必须能恢复。这样的回滚包比一句“数据库有备份”更接近实际。</p><p>RSS 和前端挑战页也不能只在出问题时看。RSS route 如果把可选的 aggregate 当成必选依赖，手记为空时就可能连文章订阅一起 500；前端若出现浏览器检查页，则要分别检查 Vercel、CDN 和源站，而不是把它归咎于文章内容。每一层都有自己的验收方法。</p><p>博客的维护很少有戏剧性的瞬间，更多是一次次确认：状态码、时间、哈希、日志、回滚目录。它们不适合写成热闹的教程，却能让几年后的自己在升级前知道应该先看哪里。页面只是最外面的一层，真正属于自己的，是这套可以重新解释和恢复的边界。</p><p>因此我把运维动作分成读、写和切换三类。读操作可以随时做，写操作必须有快照和逐条回读，切换操作则要有维护窗口和观察时间。分类以后，很多看似紧急的动作其实可以先用只读证据把范围缩小。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/projects/build-personal-blog#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/projects/build-personal-blog</link><guid isPermaLink="true">https://blog.caiths.com/posts/projects/build-personal-blog</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Sun, 02 Aug 2026 16:00:00 GMT</pubDate></item><item><title><![CDATA[撤下旧文的那几天]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/thoughts/why-i-write-after-unpublishing-old-posts">https://blog.caiths.com/posts/thoughts/why-i-write-after-unpublishing-old-posts</a></blockquote><div><p>这次重新整理博客，我做的第一件事是撤下一批旧文。</p><p>它们没有被删除，只是暂时离开了公开列表。页面一下空了不少，那些原本挤在一起、说得很满的标题也安静下来。我原以为接下来只是逐篇润色，逐篇打开正文后才发现，有些文章已经很难接着改了。</p><p>句子本身未必差，场景甚至写得很完整，可我找不到当时的笔记，也想不起那些细节从哪里来。继续换几个形容词，只会让一段陌生的第一人称变得更顺。于是我先将它们收起来。以后找到材料，再从空白页开始；找不到，也不勉强补回去。</p><h2 id="">旧文章不需要一起翻新</h2><p>面对一整个归档页，很容易想让它们同时变得整齐。标题换成相近的格式，开头都克制一点，结尾再收得平稳一些。做完以后，过去几年像是忽然拥有了同一种语气。</p><p>可旧文之间本来就有差别。有些只是表达生硬，里面的事仍然认得；有些只剩一个还愿意继续想的问题，原来的叙述却已经接不上记忆；还有一些就属于写下它的那个时刻，今天再改反而会抹掉当时的样子。</p><p>我撤下一篇文章，通常和它够不够成熟没有关系。更直接的判断是：这段经历现在还能不能由我认下来。答案不清楚时，公开列表少一篇，比替过去补一份流畅的解释更轻松。</p><h2 id="">小事留在手记里</h2><p>整理备忘录时，我碰到很多很小的记录：早上蒸了一个紫薯，刚做出一道积分题，买了清单软件，时间却没有因此变多。</p><p>它们没有完整开头，也不负责说明这一年学会了什么。以前我总觉得这样的内容撑不起一个页面，真要写，就得在后面接上时间、选择或者成长。几句话一路加长，文章看起来完整了，原来那件小事反而被压在最下面。</p><p>这一次，我让它们停在手记里。紫薯就是一顿简单的早饭，积分题只记刚刚想通的片刻，清单软件也可以停在“买了，但日子没有自动整齐”。两三句话各自很短，放在时间线上却比统一的感悟更接近那段生活。</p><p>长文需要另一种材料。一个项目经过几次变化，旧记录和代码能接起前后，才值得慢慢展开。只剩一句话时，就让它保持一句话。归档页少一篇，不会让那天跟着消失。</p><h2 id="">记不清的地方也留下来</h2><p>有些记忆要靠旧东西才能接上。备忘录里的日期、仓库中的修改、今天还能打开的页面，各自带回一小段上下文。它们无法还原整件事，却能告诉我哪些细节当时已经存在，哪些解释是后来才有的。</p><p>沿着这些材料重写，速度不会很快。说不准的地方只好空着，已经模糊的部分也不再硬接成一个漂亮转折。文章会少几句结论，多几次停顿。</p><p>我以前觉得“已经记不清了”会削弱叙述。现在再看，它有时反而是最可靠的一句。记忆本来就不会平均保存：一个项目可能只剩某次修改特别清楚，一段生活也可能只记得一个午后。承认这些缺口以后，留下来的细节才不用承担整段人生的重量。</p><h2 id="">别人的生活留在别人的文章里</h2><p>我仍然会逛个人博客。有人把几年生活写得很充实，也愿意留下没完成的计划；有人只记最近的零散近况，读完却能记住一个很轻的瞬间。</p><p>这些文章会提醒我，章节不用一样长，结尾也可以没有答案。我会留意作者如何安排材料，为什么在某个细节停下，但不会把那段经历搬进自己的归档。大学、项目、旅行和关系都有各自的来路，换掉地点和名字，也不会变成我的生活。</p><p>参考到最后，还是要关掉网页，回到自己的记录。若引用观点，就留下出处；若只是喜欢那种松一点的节奏，就从自己记得的那件小事重新写。材料不足时空着，比借一段别人的人生填满更踏实。</p><h2 id="">空白也在时间线里</h2><p>博客停了两三年，重新打开时，很容易将“补回来”变成一张时间表：每年都要有总结，每段经历都该在归档里占一个位置。空白看起来像欠下的作业。</p><p>可那些年份没有因为少了博文就变空。事情散在别处，有的是一句备忘录，有的是后来没继续的仓库，留下来的分量并不相同。强行按年份补齐，只会让材料很少的月份承担太多解释。</p><p>现在我从还认得的部分写起。某个项目的前后能接起来，就整理那段过程；某天只有几句小记，也不催它长大；已经模糊的月份继续留白。</p><p>旧文撤下以后，过去没有被清空，博客也不会一夜之间换一副模样。以后有足够前后的事，就写成长文；只留下一个瞬间，就放进手记；仍然对不上记忆的，再等一等。</p><p>我还想继续写，是因为很多事当时不记，后来真的只剩一个标题。几年后再打开，能认出那是自己做过的事、犹豫过的问题，以及当时没有说完的话，对我已经够了。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/thoughts/why-i-write-after-unpublishing-old-posts#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/thoughts/why-i-write-after-unpublishing-old-posts</link><guid isPermaLink="true">https://blog.caiths.com/posts/thoughts/why-i-write-after-unpublishing-old-posts</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Sun, 02 Aug 2026 00:44:56 GMT</pubDate></item><item><title><![CDATA[这些早期练习还留在仓库里]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/notes/31">https://blog.caiths.com/notes/31</a></blockquote><div><p>2023 年建立的公开仓库里，还放着图书管理、茶叶商城和 2048 这类早期练习。现有证据只能说明仓库与代码存在，不能替它们补上“课程作业”的来历。</p><p>我还是愿意留着几个。它们不代表现在的水平，也不必硬塞进作品集。偶尔点开，能看见自己当时怎样命名变量、怎样组织页面，这就够了。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/notes/31#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/notes/31</link><guid isPermaLink="true">https://blog.caiths.com/notes/31</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Sat, 01 Aug 2026 10:54:15 GMT</pubDate></item><item><title><![CDATA[旧仓库不会自己变新]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/notes/32">https://blog.caiths.com/notes/32</a></blockquote><div><p>一个 2023 年建立的招聘数据爬虫，在 2026 年又有了提交。8 月 1 日的公开快照里，它有 65 个 Star。仅凭这些元数据，还不能判断三年前的代码现在能稳定跑到哪一步。</p><p>Star 不会替代码更新。旧仓库重新动起来时，第一件事未必是加功能；先把现在能确认的范围、没有验证的部分写清楚，也算一次维护。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/notes/32#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/notes/32</link><guid isPermaLink="true">https://blog.caiths.com/notes/32</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Fri, 31 Jul 2026 05:35:16 GMT</pubDate></item><item><title><![CDATA[痕迹不是计数器]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.caiths.com/posts/thoughts/trace">https://blog.caiths.com/posts/thoughts/trace</a></blockquote><div><p>我有一段时间很在意提交记录的数字。</p><p>打开仓库，先看绿色的方块，再点开一条 commit。数字往上走的时候，会有一种事情没有白做的错觉：今天留下了一条记录，明天再留一条，某个阶段就被证明存在过。可提交记录只是一个计数器，它能告诉我做过多少次保存，却不能替我解释这些保存为什么发生。</p><p>后来我开始重新看那些旧仓库。它们并不整齐，也没有一条漂亮的成长曲线。早期的代码会把界面、数据和事件处理揉在同一个文件里，函数名常常只够让当时的自己看懂；文档只写了“怎么运行”，没有写“为什么这样做”。有些仓库很久没有再打开，另一些却在隔了很久之后又出现了修补。</p><p>这些缺口并不丢人。它们只是说明代码曾经处在一个具体的时间里。人往往要先把东西做出来，等下一次遇到同样的问题，才知道上一次哪里留下了债。</p><h2 id="">三个小仓库</h2><p><code>BookManager</code> 是一个用 Python 和 PyQt5 写的图书信息管理系统，仓库带有 MIT 许可证，最早的提交在 2023 年 12 月，后来又有一轮文档和小修补。它没有被包装成什么大型系统，项目目录里能看到窗口、表单和数据操作各自留下的痕迹。现在回头看，我更在意它让我第一次必须面对的那个问题：“界面上的一个动作到底会改动什么数据？”</p><p><code>wps_script</code> 是一组放在金山文档 AirScript 环境中运行的日常任务。仓库同样以 MIT 许可证发布，最近的公开提交集中在任务适配和表格验证上。它和课堂作业不太一样：代码不是写完就结束，外部接口变了，原本能工作的任务就会沉默。维护它需要先确认输入、输出和模拟测试能证明什么，再决定哪些变化值得写进脚本。很多时候，所谓“修好了”只意味着契约测试重新通过，并不等于真实账号已经验证完毕。</p><p><code>bosszhipin_spider</code> 的公开说明把它描述为 Python 与 Pyppeteer 组成的招聘数据采集工具，仓库有 MIT 许可证，也保留了爬虫与数据合并测试。这个项目让我对“能抓到数据”和“应该怎样使用数据”之间的距离更敏感。代码可以公开，测试可以公开，真实职位数据、个人信息和站点规则却不能因为出现在一个仓库里就被随意搬走。留下代码，不等于留下所有上下文的授权。</p><p>这三个仓库没有共同的技术栈，也没有组成什么宏大的产品线。它们只是分别记录了界面、接口适配和数据处理的几个阶段。把它们放在一起看，反而能看到一条更可靠的线：从“让页面动起来”，到“让任务在变化中继续工作”，再到“先问数据是否应该被处理”。</p><h2 id="">旧代码没有替我成长</h2><p>有时我会把仓库更新误认成成长。只要最近还有 commit，就好像自己仍然在前进；很久没有提交，就像某段时间被从履历里删掉了。可代码不会因为被推送到远端就自动变得成熟。一个仓库也不会因为拥有更多星标就替我承担维护责任。</p><p>真正留下来的，是一些更细小的判断：把一次重复的修补写成测试，把外部接口的假设记在文档里，把不能公开的数据挡在仓库之外，在没有必要时不再增加一个功能。它们没有明显的仪式感，甚至很少出现在项目介绍里，却比一个好看的数字更接近工程经验。</p><p>我现在打开旧仓库，先看的不再是提交总数，而是三个问题。这个项目当时想解决的具体麻烦是什么？哪些地方只在我的电脑上成立？如果今天交给另一个人，他能不能从文档和测试里判断边界？答案往往不够漂亮，但至少可以继续往下做。</p><h2 id="">让痕迹保留原来的重量</h2><p>代码和照片有一点相似。照片能证明某个瞬间被按下快门，却不能自动说明当时的人在想什么；提交能证明文件被保存过，却不能替我补写当时的动机。若要把旧项目写成文章，最容易犯的错就是给它加上一条顺滑的故事线，把零散的修补说成早已规划好的路线。</p><p>我不想再这样处理自己的记录。能从公开仓库核对的，就写仓库实际呈现的内容；没有证据的数字和场景，就删掉；涉及他人和第三方数据的部分，宁可留在原处。文章可以有情绪，但情绪不能替事实增加一条提交记录。</p><p>这些项目以后也许还会被打开，也许会一直停在某个旧版本。它们不会替我证明已经成为怎样的人，只会安静地告诉我：在某些具体的下午，我曾经把问题拆开，试着让下一次运行少一点意外。</p><p>这就够了。痕迹不是计数器，也不是一份需要不断美化的成绩单。它只是让过去保持可核对，让后来的人，包括未来的我，知道一段代码从哪里来，又在哪些地方需要重新开始。</p></div><p style="text-align:right"><a href="https://blog.caiths.com/posts/thoughts/trace#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.caiths.com/posts/thoughts/trace</link><guid isPermaLink="true">https://blog.caiths.com/posts/thoughts/trace</guid><dc:creator><![CDATA[喵内]]></dc:creator><pubDate>Thu, 30 Jul 2026 01:00:00 GMT</pubDate></item></channel></rss>