<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>yuluo</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://yuluod.github.io/icon.png</icon>
  <id>https://yuluod.github.io/</id>
  <link href="https://yuluod.github.io/" rel="alternate"/>
  <link href="https://yuluod.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, yuluo</rights>
  <subtitle>记录Python、Linux、macOS、Android、Git等技术笔记与开发实践</subtitle>
  <title>雨落杂记</title>
  <updated>2026-05-26T15:32:02.671Z</updated>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="NAS" scheme="https://yuluod.github.io/categories/NAS/"/>
    <category term="UNRAID" scheme="https://yuluod.github.io/tags/UNRAID/"/>
    <content>
      <![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>最近在折腾 Unraid 7.3 的 Internal Boot，也就是从传统 USB 启动迁移到内部 SSD 启动。</p><p>原本的目标很简单：</p><blockquote><p>用一块企业级 SATA SSD 替代 U 盘，作为 Unraid 的内部启动盘，提高稳定性。</p></blockquote><p>但实际迁移过程中，顺带暴露出了一堆隐藏问题：</p><ol><li>Cache Pool 中一块 NVMe 已经出现真实介质错误；</li><li>Btrfs RAID1 虽然能自修复，但并不代表硬盘健康；</li><li>512G 临时盘无法直接 replace 1T cache 盘；</li><li>Btrfs allocated chunk 和实际 used 不是一回事；</li><li>Internal Boot 写入成功后，BIOS 仍然无法从 SSD 启动；</li><li>LSI SAS2308 HBA 卡上的 SSD 无法被 BIOS 当作启动盘；</li><li>TPM 许可迁移后出现多个 key 文件冲突；</li><li>迁移后 License 页面显示过期时间，引发对 legacy lifetime updates 的疑问。</li></ol><p>这篇文章记录完整处理过程，供后续折腾 Unraid 7.3 Internal Boot、Btrfs cache pool 和 TPM license 的人参考。</p><span id="more"></span><hr><h2 id="二、原始环境"><a href="#二、原始环境" class="headerlink" title="二、原始环境"></a>二、原始环境</h2><p>当前机器大致结构如下：</p><table><thead><tr><th>设备</th><th>用途</th></tr></thead><tbody><tr><td>USB 启动盘</td><td>原 Unraid 启动盘</td></tr><tr><td>240G 企业级 SATA SSD</td><td>准备作为 Internal Boot</td></tr><tr><td>1T NVMe</td><td>原 cache pool 成员之一，健康</td></tr><tr><td>1T NVMe</td><td>原 cache pool 成员之一，后续确认故障</td></tr><tr><td>512G NVMe</td><td>临时用于替换故障 cache 盘</td></tr><tr><td>多块 HDD</td><td>Unraid Array 数据盘</td></tr><tr><td>LSI SAS2308 HBA</td><td>连接部分 SATA&#x2F;SAS 设备</td></tr></tbody></table><hr><h2 id="三、第一次发现问题：Btrfs-scrub-报错但可修复"><a href="#三、第一次发现问题：Btrfs-scrub-报错但可修复" class="headerlink" title="三、第一次发现问题：Btrfs scrub 报错但可修复"></a>三、第一次发现问题：Btrfs scrub 报错但可修复</h2><p>一开始执行 cache pool 的 Btrfs scrub：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs scrub start -B /mnt/cache<br></code></pre></td></tr></table></figure><p>第一次结果出现大量错误：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">Error summary: read=256 verify=20 csum=2937<br>Corrected: 3213<br>Uncorrectable: 0<br>Unverified: 0<br></code></pre></td></tr></table></figure><p>第二次 scrub 又变成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Error summary: no errors found<br></code></pre></td></tr></table></figure><p>表面看似没问题，因为：</p><ul><li><code>Corrected</code> 不为 0；</li><li><code>Uncorrectable = 0</code>；</li><li>第二次 scrub 已经无错误。</li></ul><p>但这其实只是说明：</p><blockquote><p>Btrfs RAID1 成功从另一块健康盘读取正确副本，并修复了错误数据。</p></blockquote><p>这不代表故障盘健康。</p><hr><h2 id="四、真正的硬盘故障证据：device-stats-和-dmesg"><a href="#四、真正的硬盘故障证据：device-stats-和-dmesg" class="headerlink" title="四、真正的硬盘故障证据：device stats 和 dmesg"></a>四、真正的硬盘故障证据：device stats 和 dmesg</h2><p>继续查看 Btrfs device stats：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs device stats /mnt/cache<br></code></pre></td></tr></table></figure><p>结果非常明显：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs text">[/dev/nvme1n1p1].write_io_errs    29513266<br>[/dev/nvme1n1p1].read_io_errs     88468<br>[/dev/nvme1n1p1].flush_io_errs    1244217<br>[/dev/nvme1n1p1].corruption_errs  38172<br>[/dev/nvme1n1p1].generation_errs  5<br><br>[/dev/nvme0n1p1].write_io_errs    0<br>[/dev/nvme0n1p1].read_io_errs     0<br>[/dev/nvme0n1p1].flush_io_errs    0<br>[/dev/nvme0n1p1].corruption_errs  0<br>[/dev/nvme0n1p1].generation_errs  0<br></code></pre></td></tr></table></figure><p>可以确认：</p><blockquote><p><code>/dev/nvme1n1p1</code> 是故障盘，<code>/dev/nvme0n1p1</code> 是健康盘。</p></blockquote><p>再看 dmesg：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">dmesg | grep -i nvme<br></code></pre></td></tr></table></figure><p>其中出现了关键错误：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">critical medium error, dev nvme1n1<br>I/O Error (sct 0x2 / sc 0x81)<br></code></pre></td></tr></table></figure><p>这类错误已经不是 APST、省电、驱动、PCIe reset 之类的问题，而是典型的：</p><blockquote><p>SSD 介质 &#x2F; 数据完整性错误。</p></blockquote><p>即使 Windows 下 SMART 显示“健康”，只要出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Media and Data Integrity Errors &gt; 0<br></code></pre></td></tr></table></figure><p>就不能再把它当作可靠盘使用。</p><p>这块故障 NVMe 后续拆下来重新查看，SMART 中也能看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Media and Data Integrity Errors = 36<br></code></pre></td></tr></table></figure><p>这个值正常应该是 0。</p><hr><h2 id="五、关于-SMART-的一个误区"><a href="#五、关于-SMART-的一个误区" class="headerlink" title="五、关于 SMART 的一个误区"></a>五、关于 SMART 的一个误区</h2><p>这次有一个很典型的误区：</p><blockquote><p>Windows 工具显示“健康”不代表盘真的适合继续做 NAS cache。</p></blockquote><p>很多 SMART 工具主要看：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Critical Warning<br></code></pre></td></tr></table></figure><p>只要它是 0，就显示“健康”。</p><p>但对 NAS &#x2F; Btrfs &#x2F; ZFS 来说，更重要的是：</p><table><thead><tr><th>指标</th><th>要求</th></tr></thead><tbody><tr><td>Media and Data Integrity Errors</td><td>必须为 0</td></tr><tr><td>Error Information Log Entries</td><td>越少越好，需看具体类型</td></tr><tr><td>Reallocated &#x2F; Pending &#x2F; Uncorrectable</td><td>必须为 0</td></tr><tr><td>Btrfs corruption_errs</td><td>必须为 0</td></tr><tr><td>scrub uncorrectable</td><td>必须为 0</td></tr></tbody></table><p>这次坏盘的问题就是：</p><ul><li>Windows 健康状态正常；</li><li>但 Btrfs 已经发现大量读写、flush、corruption 错误；</li><li>dmesg 已经出现 critical medium error；</li><li>SMART 里 Media and Data Integrity Errors 已经不是 0。</li></ul><p>所以结论很明确：</p><blockquote><p>这块盘还能亮、还能读，不等于还能作为 cache &#x2F; appdata &#x2F; docker &#x2F; VM 盘继续使用。</p></blockquote><hr><h2 id="六、512G-替换-1T-cache-盘为什么失败？"><a href="#六、512G-替换-1T-cache-盘为什么失败？" class="headerlink" title="六、512G 替换 1T cache 盘为什么失败？"></a>六、512G 替换 1T cache 盘为什么失败？</h2><p>手边刚好有一块 512G NVMe，于是想直接替换故障的 1T 1T NVMe。</p><p>一开始查看：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs filesystem show<br></code></pre></td></tr></table></figure><p>看到 cache pool：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">Total devices 2 FS bytes used 372.30GiB<br>devid 1 size 953.87GiB used 375.03GiB path /dev/nvme1n1p1<br>devid 2 size 953.87GiB used 375.03GiB path /dev/nvme0n1p1<br></code></pre></td></tr></table></figure><p>这里有一个关键点：</p><blockquote><p><code>FS bytes used</code> 只有 372GiB，说明真实逻辑数据量并不大。</p></blockquote><p>理论上 512G 应该够。</p><p>但在 Unraid GUI 或 Btrfs replace 时提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">wrong pool state<br>replace device too small<br></code></pre></td></tr></table></figure><p>原因在于：</p><blockquote><p>Btrfs replace 看的不只是实际 used，还会受到 allocated chunk 的影响。</p></blockquote><p>也就是说，Btrfs 可能只用了 372G 数据，但已经分配了更大的 chunk。此前 usage 中曾看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Device allocated: 741.06GiB<br></code></pre></td></tr></table></figure><p>所以直接用 512G 替换会失败。</p><hr><h2 id="七、解决办法：先降级为单盘，再清理-RAID1-allocation"><a href="#七、解决办法：先降级为单盘，再清理-RAID1-allocation" class="headerlink" title="七、解决办法：先降级为单盘，再清理 RAID1 allocation"></a>七、解决办法：先降级为单盘，再清理 RAID1 allocation</h2><p>拆掉故障盘后，Btrfs pool 进入 degraded 状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs filesystem show<br></code></pre></td></tr></table></figure><p>显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs text">warning, device 1 is missing<br><br>Label: none uuid: 1d44c14b...<br>Total devices 2 FS bytes used 372.28GiB<br>devid 2 size 953.87GiB used 375.03GiB path /dev/nvme0n1p1<br>*** Some devices missing<br></code></pre></td></tr></table></figure><p>此时需要先手动 degraded mount：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p /mnt/recovery<br>mount -o degraded /dev/nvme0n1p1 /mnt/recovery<br></code></pre></td></tr></table></figure><p>确认数据还能看到后，开始 balance，把 RAID1 profile 转成 single：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs balance start -dconvert=single -mconvert=dup /mnt/cache<br></code></pre></td></tr></table></figure><p>最终状态变成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs text">pool status: ONLINE<br>Device size: 953.87GiB<br>Device allocated: 379.06GiB<br>Used: 373.54GiB<br>Data ratio: 1.00<br>Metadata ratio: 2.00<br>Multiple profiles: no<br><br>Data single<br>Metadata DUP<br>System DUP<br></code></pre></td></tr></table></figure><p>这说明：</p><ul><li>cache pool 已经从 RAID1 降级为单盘；</li><li>不再 missing device；</li><li>allocated 已经降到 379G；</li><li>未来再加 512G 重新转 RAID1 就可行了。</li></ul><p>当前状态下，其实已经安全很多：</p><table><thead><tr><th>项目</th><th>状态</th></tr></thead><tbody><tr><td>pool</td><td>ONLINE</td></tr><tr><td>数据</td><td>single</td></tr><tr><td>metadata</td><td>DUP</td></tr><tr><td>corruption errors</td><td>0</td></tr><tr><td>missing device</td><td>无</td></tr><tr><td>allocated</td><td>379G</td></tr></tbody></table><hr><h2 id="八、为什么没有立刻把-512G-加回-RAID1？"><a href="#八、为什么没有立刻把-512G-加回-RAID1？" class="headerlink" title="八、为什么没有立刻把 512G 加回 RAID1？"></a>八、为什么没有立刻把 512G 加回 RAID1？</h2><p>虽然现在可以加回 512G，但我最后没有急着操作。</p><p>原因是：</p><ol><li>当前 cache 已经稳定为单盘；</li><li>512G 是国产消费级 NVMe；</li><li>刚经历一次故障和大量 balance，不宜立刻继续重 IO；</li><li>后续计划等 SSD 价格回落后，换 1T 企业 SATA &#x2F; 企业 NVMe 更合适。</li></ol><p>如果后面要重新加回 512G RAID1，可以在 GUI 中把 512G 加入 pool，然后执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt/cache<br></code></pre></td></tr></table></figure><p>但当前更稳妥的是：</p><blockquote><p>先单盘稳定运行，建立 appdata 备份，再决定是否恢复 RAID1。</p></blockquote><hr><h2 id="九、240G-S4610-做-Internal-Boot-是否合适？"><a href="#九、240G-S4610-做-Internal-Boot-是否合适？" class="headerlink" title="九、240G S4610 做 Internal Boot 是否合适？"></a>九、240G S4610 做 Internal Boot 是否合适？</h2><p>这次准备作为 Internal Boot 的盘是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">&lt;ENTERPRISE-SATA-SSD-MODEL-REDACTED&gt;<br>240G 企业级 SATA SSD<br></code></pre></td></tr></table></figure><p>SMART 状态如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs text">Reallocated sector count = 0<br>Current pending sector = 0<br>Offline uncorrectable = 0<br>CRC error count = 0<br>Program fail count total = 0<br>Erase fail count total = 0<br>Percent life remaining = 100%<br>Power on hours = 41479<br>Temperature = 27℃<br></code></pre></td></tr></table></figure><p>虽然通电时间已经 4.7 年，但对企业 SATA SSD 来说不算夸张。</p><p>而且它的关键指标很干净：</p><ul><li>无重映射；</li><li>无 pending；</li><li>无 uncorrectable；</li><li>无 CRC；</li><li>PLP 相关项正常；</li><li>剩余寿命 100%。</li></ul><p>这类企业 SATA 盘做 Unraid Internal Boot 非常合适，因为 Unraid boot 盘主要存：</p><ul><li>config；</li><li>license；</li><li>plugins；</li><li>docker templates；</li><li>系统启动文件。</li></ul><p>并不承载高频 appdata &#x2F; database &#x2F; VM 写入。</p><p>所以 240G 甚至是非常富裕的容量。</p><hr><h2 id="十、Internal-Boot-页面中“专用启动池”和“启动-数据池”的区别"><a href="#十、Internal-Boot-页面中“专用启动池”和“启动-数据池”的区别" class="headerlink" title="十、Internal Boot 页面中“专用启动池”和“启动 + 数据池”的区别"></a>十、Internal Boot 页面中“专用启动池”和“启动 + 数据池”的区别</h2><p>Unraid 7.3 Internal Boot 创建时，会让你选择池模式：</p><ol><li>专用启动池；</li><li>启动 + 数据池。</li></ol><p>二者区别如下：</p><table><thead><tr><th>模式</th><th>说明</th><th>是否推荐</th></tr></thead><tbody><tr><td>专用启动池</td><td>SSD 只做 Unraid boot</td><td>推荐</td></tr><tr><td>启动 + 数据池</td><td>SSD 同时做 boot 和普通 data pool</td><td>不推荐当前环境使用</td></tr></tbody></table><p>我的选择是：</p><blockquote><p>专用启动池。</p></blockquote><p>原因很简单：</p><ul><li>Boot 和 cache 应该隔离；</li><li>启动盘不应承载 Docker &#x2F; appdata &#x2F; VM；</li><li>一旦启动盘坏，不应影响普通数据池；</li><li>这种结构更接近传统 USB boot + 独立 cache 的稳定模型。</li></ul><p>最终设计：</p><table><thead><tr><th>设备</th><th>用途</th></tr></thead><tbody><tr><td>240G S4610</td><td>Internal Boot</td></tr><tr><td>1T Toshiba NVMe</td><td>Cache</td></tr><tr><td>512G NVMe</td><td>临时备用 &#x2F; 未来 mirror</td></tr><tr><td>HDD Array</td><td>主数据</td></tr></tbody></table><hr><h2 id="十一、Internal-Boot-写入成功，但拔-U-盘无法启动"><a href="#十一、Internal-Boot-写入成功，但拔-U-盘无法启动" class="headerlink" title="十一、Internal Boot 写入成功，但拔 U 盘无法启动"></a>十一、Internal Boot 写入成功，但拔 U 盘无法启动</h2><p>Internal Boot 创建成功后，<code>lsblk -f</code> 显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">sdc<br>├─sdc1<br>├─sdc2 vfat FAT32 EFI<br>├─sdc3 zfs_member flash<br>└─sdc4<br></code></pre></td></tr></table></figure><p>说明：</p><ul><li>EFI 分区存在；</li><li>ZFS flash boot pool 存在；</li><li>Internal Boot 本身已经写入成功。</li></ul><p>进一步检查 EFI 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p /mnt/bootssd<br>mount /dev/sdc2 /mnt/bootssd<br>find /mnt/bootssd<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">/mnt/bootssd<br>/mnt/bootssd/EFI<br>/mnt/bootssd/EFI/BOOT<br>/mnt/bootssd/EFI/BOOT/BOOTX64.EFI<br></code></pre></td></tr></table></figure><p>这说明 EFI fallback 文件也存在。</p><p>然后通过 <code>efibootmgr</code> 手动创建 EFI 启动项：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">efibootmgr -c -d /dev/sdc -p 2 -L <span class="hljs-string">&quot;UnraidSSD&quot;</span> -l <span class="hljs-string">&#x27;\EFI\BOOT\BOOTX64.EFI&#x27;</span><br>efibootmgr -o 0000<br></code></pre></td></tr></table></figure><p>最终：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">BootCurrent: 0001<br>BootOrder: 0000<br>Boot0000* UnraidSSD<br>Boot0001* UEFI: USB-Boot-Device, Partition 1<br></code></pre></td></tr></table></figure><p>看起来已经正确。</p><p>但拔掉 U 盘后，机器仍然直接进入 BIOS，Boot 页面看不到这个 SSD。</p><hr><h2 id="十二、最终确认：问题出在-LSI-SAS2308-HBA-启动支持"><a href="#十二、最终确认：问题出在-LSI-SAS2308-HBA-启动支持" class="headerlink" title="十二、最终确认：问题出在 LSI SAS2308 HBA 启动支持"></a>十二、最终确认：问题出在 LSI SAS2308 HBA 启动支持</h2><p>最后发现一个关键事实：</p><blockquote><p>这块 S4610 是通过 LSI SAS2308 HBA 连接到主板的。</p></blockquote><p>这就解释了所有现象。</p><p>系统启动后，Linux 能看到 SSD：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">sdc 223.6G &lt;ENTERPRISE-SATA-SSD-MODEL-REDACTED&gt;<br></code></pre></td></tr></table></figure><p>但 BIOS 阶段看不到启动项。</p><p>这是典型的：</p><blockquote><p>HBA 在操作系统里可识别，但 BIOS &#x2F; UEFI 阶段不提供可启动路径。</p></blockquote><p>LSI SAS2308 这类 HBA 分几个部分：</p><table><thead><tr><th>部分</th><th>作用</th></tr></thead><tbody><tr><td>IT Firmware</td><td>操作系统识别磁盘</td></tr><tr><td>Legacy BIOS ROM</td><td>Legacy 启动</td></tr><tr><td>UEFI ROM</td><td>UEFI 启动</td></tr></tbody></table><p>很多二手 HBA 卡只刷了 IT firmware，没有刷 UEFI boot ROM，或者主板 BIOS 不支持从这张卡启动。</p><p>所以现象就是：</p><table><thead><tr><th>阶段</th><th>状态</th></tr></thead><tbody><tr><td>Unraid 启动后</td><td>能看到 SSD</td></tr><tr><td>EFI 分区</td><td>正常</td></tr><tr><td>BOOTX64.EFI</td><td>正常</td></tr><tr><td>efibootmgr</td><td>能创建 entry</td></tr><tr><td>BIOS 启动阶段</td><td>看不到 SSD &#x2F; 无法启动</td></tr></tbody></table><p>最终结论：</p><blockquote><p>Internal Boot 本身没有问题，问题在于启动 SSD 挂在 HBA 后，主板 BIOS 无法从该 HBA 上的 SATA SSD 启动。</p></blockquote><p>解决方案有两个：</p><h3 id="方案-A：启动-SSD-接板载-SATA"><a href="#方案-A：启动-SSD-接板载-SATA" class="headerlink" title="方案 A：启动 SSD 接板载 SATA"></a>方案 A：启动 SSD 接板载 SATA</h3><p>这是最推荐方案。</p><table><thead><tr><th>设备</th><th>连接方式</th></tr></thead><tbody><tr><td>S4610 Boot SSD</td><td>板载 SATA</td></tr><tr><td>HDD 数据盘</td><td>LSI SAS2308</td></tr><tr><td>NVMe cache</td><td>主板 M.2</td></tr></tbody></table><p>这是最稳、最少玄学的方案。</p><h3 id="方案-B：给-SAS2308-HBA-补刷-UEFI-ROM"><a href="#方案-B：给-SAS2308-HBA-补刷-UEFI-ROM" class="headerlink" title="方案 B：给 SAS2308 HBA 补刷 UEFI ROM"></a>方案 B：给 SAS2308 HBA 补刷 UEFI ROM</h3><p>理论可行，但不推荐作为首选。</p><p>原因：</p><ul><li>有刷错风险；</li><li>不同 OEM 卡兼容性不同；</li><li>主板 BIOS 仍可能不认；</li><li>HBA 初始化会拖慢启动；</li><li>启动盘挂 HBA 本身就不如接板载 SATA 稳。</li></ul><hr><h2 id="十三、BIOS-设置踩坑"><a href="#十三、BIOS-设置踩坑" class="headerlink" title="十三、BIOS 设置踩坑"></a>十三、BIOS 设置踩坑</h2><p>过程中也排查过华硕 BIOS 设置。</p><p>关键设置如下：</p><table><thead><tr><th>项目</th><th>推荐值</th></tr></thead><tbody><tr><td>Secure Boot &#x2F; OS Type</td><td>Other OS</td></tr><tr><td>Launch CSM</td><td>Disabled</td></tr><tr><td>Boot from Storage Devices</td><td>UEFI only</td></tr><tr><td>Fast Boot</td><td>Disabled</td></tr><tr><td>AMI Native NVMe Driver Support</td><td>On</td></tr></tbody></table><p>其中：</p><ul><li>Secure Boot 不关，Unraid EFI 可能无法启动；</li><li>CSM 开启时，有些华硕 BIOS 不扫描 Linux EFI；</li><li>Fast Boot 可能跳过部分存储扫描；</li><li>但这些都调整后仍不行，最终才确认是 HBA 启动支持问题。</li></ul><hr><h2 id="十四、TPM-许可迁移后的多个-key-文件问题"><a href="#十四、TPM-许可迁移后的多个-key-文件问题" class="headerlink" title="十四、TPM 许可迁移后的多个 key 文件问题"></a>十四、TPM 许可迁移后的多个 key 文件问题</h2><p>Internal Boot 成功后，Unraid 进入注册页面，提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">存在多个许可证密钥<br>License key type: Invalid<br></code></pre></td></tr></table></figure><p>检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> /boot/config/*.key<br></code></pre></td></tr></table></figure><p>发现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">/boot/config/Basic.key<br>/boot/config/Plus.key<br></code></pre></td></tr></table></figure><p>也就是 <code>/boot/config</code> 下有多个 key 文件。</p><p>Unraid 7.3 TPM licensing 对 key 文件更严格，只能保留一个有效 key。</p><p>最终删除旧的 Basic.key：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">rm</span> /boot/config/Basic.key<br><span class="hljs-built_in">sync</span><br>reboot<br></code></pre></td></tr></table></figure><p>然后重新更新密钥，许可恢复。</p><hr><h2 id="十五、License-Expires-After-是什么？"><a href="#十五、License-Expires-After-是什么？" class="headerlink" title="十五、License Expires After 是什么？"></a>十五、License Expires After 是什么？</h2><p>迁移 TPM license 后，页面显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">Unraid Plus License<br>GUID: &lt;LICENSE-GUID-REDACTED&gt;<br>License Expires After: 2025-09-11<br></code></pre></td></tr></table></figure><p>这个地方容易误解。</p><p>它不是说系统到期不能用了。</p><p>Unraid 新许可体系里：</p><ul><li>License 本身是永久的；</li><li>Expire 通常指更新资格；</li><li>到期后当前版本仍可继续使用；</li><li>但是否能继续享受 lifetime updates，要看账号里 Manage Purchases 的状态。</li></ul><p>本次账号的 Manage Purchases 中显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">OS Updates: Lifetime<br></code></pre></td></tr></table></figure><p>因此这里需要向 Unraid support 确认：</p><blockquote><p>TPM 迁移后，legacy Plus license 是否仍然保留 lifetime OS updates，以及页面中的 expires after 是否只是显示问题或迁移后的兼容字段。</p></blockquote><hr><h2 id="十六、最终状态"><a href="#十六、最终状态" class="headerlink" title="十六、最终状态"></a>十六、最终状态</h2><p>目前处理后的状态：</p><table><thead><tr><th>项目</th><th>状态</th></tr></thead><tbody><tr><td>故障 1T NVMe</td><td>已拆除，不再用于生产数据</td></tr><tr><td>Cache pool</td><td>单盘 online，Data single，Metadata DUP</td></tr><tr><td>Btrfs corruption</td><td>归零</td></tr><tr><td>512G NVMe</td><td>可作为后续临时 mirror</td></tr><tr><td>240G S4610</td><td>Internal Boot 已创建成功</td></tr><tr><td>Internal Boot EFI</td><td>正常存在</td></tr><tr><td>BIOS 启动失败原因</td><td>HBA 不支持 &#x2F; 未提供 UEFI boot</td></tr><tr><td>License 多 key 问题</td><td>已定位并处理</td></tr><tr><td>TPM License</td><td>已迁移，但 lifetime updates 状态需向官方确认</td></tr></tbody></table><hr><h2 id="十七、这次折腾的经验总结"><a href="#十七、这次折腾的经验总结" class="headerlink" title="十七、这次折腾的经验总结"></a>十七、这次折腾的经验总结</h2><h3 id="1-Btrfs-scrub-能修复，不代表硬盘健康"><a href="#1-Btrfs-scrub-能修复，不代表硬盘健康" class="headerlink" title="1. Btrfs scrub 能修复，不代表硬盘健康"></a>1. Btrfs scrub 能修复，不代表硬盘健康</h3><p>如果 scrub 出现 corrected，而不是 uncorrectable，只说明镜像起作用了。</p><p>真正要看：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">btrfs device stats /mnt/cache<br>dmesg | grep -i nvme<br>smartctl -a /dev/nvmeXn1<br></code></pre></td></tr></table></figure><p>尤其是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">Media and Data Integrity Errors<br>critical medium error<br>corruption_errs<br></code></pre></td></tr></table></figure><p>这些才是判断硬盘是否可靠的关键。</p><hr><h3 id="2-Windows-SMART-健康状态不能作为-NAS-可靠性判断"><a href="#2-Windows-SMART-健康状态不能作为-NAS-可靠性判断" class="headerlink" title="2. Windows SMART 健康状态不能作为 NAS 可靠性判断"></a>2. Windows SMART 健康状态不能作为 NAS 可靠性判断</h3><p>“健康 &#x2F; 优秀”只适合普通用户粗略参考。</p><p>NAS &#x2F; Btrfs &#x2F; ZFS 环境应该更严格：</p><ul><li>Media Errors 必须为 0；</li><li>corruption 必须为 0；</li><li>pending &#x2F; uncorrectable 必须为 0；</li><li>scrub 不应反复 corrected。</li></ul><hr><h3 id="3-Btrfs-的-used-和-allocated-不是一回事"><a href="#3-Btrfs-的-used-和-allocated-不是一回事" class="headerlink" title="3. Btrfs 的 used 和 allocated 不是一回事"></a>3. Btrfs 的 used 和 allocated 不是一回事</h3><p>这次最大坑之一是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">FS bytes used 372G<br></code></pre></td></tr></table></figure><p>但 replace 512G 仍然失败。</p><p>原因是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Device allocated<br></code></pre></td></tr></table></figure><p>过高。</p><p>Btrfs replace &#x2F; shrink &#x2F; balance 过程中，allocated chunk 非常关键。</p><hr><h3 id="4-降容量-replace-很麻烦，重建-pool-有时反而更干净"><a href="#4-降容量-replace-很麻烦，重建-pool-有时反而更干净" class="headerlink" title="4. 降容量 replace 很麻烦，重建 pool 有时反而更干净"></a>4. 降容量 replace 很麻烦，重建 pool 有时反而更干净</h3><p>从 1T RAID1 换到 512G RAID1，理论上只要 used 足够小就行。</p><p>但实际会受：</p><ul><li>allocated chunk；</li><li>metadata；</li><li>profile；</li><li>balance 状态；</li></ul><p>影响。</p><p>如果数据量可控，有时备份 appdata 后重建 cache pool 反而更简单。</p><hr><h3 id="5-Internal-Boot-最适合用“专用启动池”"><a href="#5-Internal-Boot-最适合用“专用启动池”" class="headerlink" title="5. Internal Boot 最适合用“专用启动池”"></a>5. Internal Boot 最适合用“专用启动池”</h3><p>不建议把 boot SSD 同时当普通 data pool 使用。</p><p>更稳结构是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">小企业 SATA SSD = 专用 Internal Boot<br>NVMe / SSD = cache<br>HDD = array<br></code></pre></td></tr></table></figure><hr><h3 id="6-企业-SATA-SSD-做-boot-很合适"><a href="#6-企业-SATA-SSD-做-boot-很合适" class="headerlink" title="6. 企业 SATA SSD 做 boot 很合适"></a>6. 企业 SATA SSD 做 boot 很合适</h3><p>240G S4610 这种盘虽然不新，但做 Unraid Boot 非常合适：</p><ul><li>稳定；</li><li>有 SMART；</li><li>有 PLP；</li><li>发热低；</li><li>写入压力小；</li><li>比 USB 盘可靠得多。</li></ul><hr><h3 id="7-HBA-上能被-Linux-识别，不代表能被-BIOS-启动"><a href="#7-HBA-上能被-Linux-识别，不代表能被-BIOS-启动" class="headerlink" title="7. HBA 上能被 Linux 识别，不代表能被 BIOS 启动"></a>7. HBA 上能被 Linux 识别，不代表能被 BIOS 启动</h3><p>这是这次 Internal Boot 最大的坑。</p><p>LSI SAS2308 这类 HBA：</p><ul><li>进系统后识别磁盘没问题；</li><li>但 BIOS 阶段不一定能从它启动；</li><li>取决于是否有 UEFI ROM、主板 BIOS 是否支持。</li></ul><p>如果启动盘接在 HBA 上，遇到 BIOS 看不到启动项，优先怀疑 HBA boot support。</p><p>最稳方案：</p><blockquote><p>启动盘接板载 SATA，HBA 只管数据盘。</p></blockquote><hr><h3 id="8-TPM-license-迁移后，注意-boot-config-下只能留一个-key"><a href="#8-TPM-license-迁移后，注意-boot-config-下只能留一个-key" class="headerlink" title="8. TPM license 迁移后，注意 /boot/config 下只能留一个 key"></a>8. TPM license 迁移后，注意 <code>/boot/config</code> 下只能留一个 key</h3><p>如果出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">存在多个许可证密钥<br>License key type: Invalid<br></code></pre></td></tr></table></figure><p>检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> /boot/config/*.key<br></code></pre></td></tr></table></figure><p>只保留当前有效的 key，删除旧 key。</p><hr><h3 id="9-Legacy-Plus-用户迁移-TPM-后，要确认-lifetime-updates-状态"><a href="#9-Legacy-Plus-用户迁移-TPM-后，要确认-lifetime-updates-状态" class="headerlink" title="9. Legacy Plus 用户迁移 TPM 后，要确认 lifetime updates 状态"></a>9. Legacy Plus 用户迁移 TPM 后，要确认 lifetime updates 状态</h3><p>如果页面显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">License Expires After<br></code></pre></td></tr></table></figure><p>但账号里显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">OS Updates: Lifetime<br></code></pre></td></tr></table></figure><p>建议联系 Unraid support 确认。</p><p>尤其是 legacy Basic &#x2F; Plus &#x2F; Pro 用户，理论上应保留 lifetime updates。</p><hr><h2 id="十八、最终推荐架构"><a href="#十八、最终推荐架构" class="headerlink" title="十八、最终推荐架构"></a>十八、最终推荐架构</h2><p>基于这次踩坑，比较稳的 Unraid 7.3 架构是：</p><table><thead><tr><th>层级</th><th>推荐设备</th></tr></thead><tbody><tr><td>Boot</td><td>板载 SATA 上的小企业 SSD</td></tr><tr><td>Cache</td><td>企业 SATA &#x2F; 企业 NVMe &#x2F; 可靠消费 NVMe</td></tr><tr><td>Cache 冗余</td><td>Btrfs RAID1 或 ZFS mirror</td></tr><tr><td>Array</td><td>HDD + parity</td></tr><tr><td>HBA</td><td>只接数据盘，不接启动盘</td></tr><tr><td>Backup</td><td>定期备份 appdata + boot config</td></tr></tbody></table><p>当前更合理的最终结构应该是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">S4610 240G       → 板载 SATA，Internal Boot<br>1T NVMe  → Cache<br>512G NVMe        → 临时备用 / 后续 mirror<br>HDD Array        → 主数据<br>LSI SAS2308      → 只接 HDD 数据盘<br></code></pre></td></tr></table></figure><hr><h2 id="十九、结语"><a href="#十九、结语" class="headerlink" title="十九、结语"></a>十九、结语</h2><p>这次迁移本来只是想把 Unraid 从 U 盘启动改成内部 SSD 启动，但实际过程中暴露出的问题远比预期多。</p><p>最终最大的收获是：</p><blockquote><p>存储系统里，“能识别”和“可靠运行”是两回事；<br>“SMART 健康”和“适合 NAS 长期使用”也是两回事；<br>“EFI 文件存在”和“BIOS 能启动”仍然是两回事。</p></blockquote><p>Unraid 7.3 的 Internal Boot 是一个很好的方向，但如果启动盘挂在 HBA &#x2F; 扩展卡上，仍然可能踩 BIOS 启动支持的坑。</p><p>最稳的做法仍然是：</p><blockquote><p>启动盘接板载控制器，数据盘交给 HBA，cache 做好备份和定期 scrub。</p></blockquote><p>这次虽然过程很折腾，但也顺手发现并处理了一块已经开始出问题的 NVMe。某种意义上，Btrfs RAID1 和 scrub 算是提前救了一次数据。</p>]]>
    </content>
    <id>https://yuluod.github.io/2026/05/26/Unraid-7-3-Internal-Boot-%E8%BF%81%E7%A7%BB%E5%AE%9E%E5%BD%95%EF%BC%9A%E4%BB%8E-NVMe-%E6%95%85%E9%9A%9C%E3%80%81Btrfs-%E9%99%8D%E7%BA%A7%EF%BC%8C%E5%88%B0-TPM-%E8%AE%B8%E5%8F%AF%E4%B8%8E-HBA-%E5%90%AF%E5%8A%A8%E9%97%AE%E9%A2%98/</id>
    <link href="https://yuluod.github.io/2026/05/26/Unraid-7-3-Internal-Boot-%E8%BF%81%E7%A7%BB%E5%AE%9E%E5%BD%95%EF%BC%9A%E4%BB%8E-NVMe-%E6%95%85%E9%9A%9C%E3%80%81Btrfs-%E9%99%8D%E7%BA%A7%EF%BC%8C%E5%88%B0-TPM-%E8%AE%B8%E5%8F%AF%E4%B8%8E-HBA-%E5%90%AF%E5%8A%A8%E9%97%AE%E9%A2%98/"/>
    <published>2026-05-26T15:18:17.000Z</published>
    <summary>
      <![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>最近在折腾 Unraid 7.3 的 Internal Boot，也就是从传统 USB 启动迁移到内部 SSD 启动。</p>
<p>原本的目标很简单：</p>
<blockquote>
<p>用一块企业级 SATA SSD 替代 U 盘，作为 Unraid 的内部启动盘，提高稳定性。</p>
</blockquote>
<p>但实际迁移过程中，顺带暴露出了一堆隐藏问题：</p>
<ol>
<li>Cache Pool 中一块 NVMe 已经出现真实介质错误；</li>
<li>Btrfs RAID1 虽然能自修复，但并不代表硬盘健康；</li>
<li>512G 临时盘无法直接 replace 1T cache 盘；</li>
<li>Btrfs allocated chunk 和实际 used 不是一回事；</li>
<li>Internal Boot 写入成功后，BIOS 仍然无法从 SSD 启动；</li>
<li>LSI SAS2308 HBA 卡上的 SSD 无法被 BIOS 当作启动盘；</li>
<li>TPM 许可迁移后出现多个 key 文件冲突；</li>
<li>迁移后 License 页面显示过期时间，引发对 legacy lifetime updates 的疑问。</li>
</ol>
<p>这篇文章记录完整处理过程，供后续折腾 Unraid 7.3 Internal Boot、Btrfs cache pool 和 TPM license 的人参考。</p>]]>
    </summary>
    <title>Unraid 7.3 Internal Boot 迁移实录：从 NVMe 故障、Btrfs 降级，到 TPM 许可与 HBA 启动问题</title>
    <updated>2026-05-26T15:32:02.671Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <category term="Workspace" scheme="https://yuluod.github.io/tags/Workspace/"/>
    <content>
      <![CDATA[<h2 id="Python-新一代-Monorepo-工程组织方式"><a href="#Python-新一代-Monorepo-工程组织方式" class="headerlink" title="Python 新一代 Monorepo 工程组织方式"></a>Python 新一代 Monorepo 工程组织方式</h2><hr><h1 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h1><p>最近打开 PyCharm，你可能会看到这样一个提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">Enable and try workspace mode Beta<br><br>A large project can be split into smaller parts<br>(like backend, API, and shared code),<br>making it easier to organize and maintain.<br></code></pre></td></tr></table></figure><p>很多人第一反应：</p><blockquote><p>“这是啥？”</p><p>“和普通 Project 有什么区别？”</p><p>“是不是类似 pnpm workspace？”</p><p>“Python 终于开始搞 monorepo 了吗？”</p></blockquote><p>实际上：</p><p>这个功能背后，代表的是 Python 工程组织方式的一次重大升级。</p><p>而它真正对应的生态，其实是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">uv + pyproject.toml + workspace<br></code></pre></td></tr></table></figure><p>这篇文章会从：</p><ul><li>为什么需要 Workspace</li><li>什么是 Monorepo</li><li>uv Workspace 怎么用</li><li>PyCharm Workspace Mode 如何配合</li><li>实际部署怎么做</li><li>如何避免架构混乱</li></ul><p>完整讲清楚。</p><span id="more"></span><hr><h1 id="二、传统-Python-项目的问题"><a href="#二、传统-Python-项目的问题" class="headerlink" title="二、传统 Python 项目的问题"></a>二、传统 Python 项目的问题</h1><p>很多 Python 项目最开始都长这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs text">backend/<br>├── api/<br>├── utils/<br>├── services/<br>├── rag/<br>├── crawler/<br>├── scripts/<br>└── requirements.txt<br></code></pre></td></tr></table></figure><p>刚开始没问题。</p><p>但项目越来越大后：</p><ul><li>import 开始混乱</li><li>共用代码越来越多</li><li>utils 到处复制</li><li>requirements.txt 爆炸</li><li>IDE indexing 越来越慢</li><li>多服务越来越难管理</li></ul><p>最后你会看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">utils_final.py<br>utils_v2.py<br>shared_new/<br>common_latest/<br></code></pre></td></tr></table></figure><p>整个工程开始失控。</p><hr><h1 id="三、为什么现在都在搞-Monorepo"><a href="#三、为什么现在都在搞-Monorepo" class="headerlink" title="三、为什么现在都在搞 Monorepo"></a>三、为什么现在都在搞 Monorepo</h1><p>现代 AI &#x2F; 后端项目越来越复杂：</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs text">AI 平台<br>├── API 服务<br>├── Worker<br>├── RAG 系统<br>├── Agent<br>├── Embedding 服务<br>├── MCP Server<br>├── Shared SDK<br>└── 管理后台<br></code></pre></td></tr></table></figure><p>如果每个都是独立仓库：</p><ul><li>版本同步困难</li><li>共用代码麻烦</li><li>SDK 更新痛苦</li><li>调试困难</li><li>本地开发复杂</li></ul><p>于是很多公司开始统一：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">一个大仓库（Monorepo）<br></code></pre></td></tr></table></figure><hr><h1 id="四、什么是-Workspace"><a href="#四、什么是-Workspace" class="headerlink" title="四、什么是 Workspace"></a>四、什么是 Workspace</h1><p>Workspace 本质上：</p><p>不是部署工具。</p><p>而是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">多项目统一开发管理<br></code></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs text">my-platform/<br>├── apps/<br>│   ├── api/<br>│   ├── worker/<br>│   └── crawler/<br>│<br>├── libs/<br>│   ├── shared/<br>│   ├── rag/<br>│   └── llm_core/<br>│<br>├── pyproject.toml<br>└── uv.lock<br></code></pre></td></tr></table></figure><p>这里：</p><ul><li>apps 是服务</li><li>libs 是共享代码</li></ul><p>整个仓库：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">一个 workspace<br></code></pre></td></tr></table></figure><hr><h1 id="五、为什么-PyCharm-要推出-Workspace-Mode"><a href="#五、为什么-PyCharm-要推出-Workspace-Mode" class="headerlink" title="五、为什么 PyCharm 要推出 Workspace Mode"></a>五、为什么 PyCharm 要推出 Workspace Mode</h1><p>以前 PyCharm 的模型：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">一个 Project = 一个工程<br></code></pre></td></tr></table></figure><p>但现在：</p><p>一个仓库可能包含：</p><ul><li>API</li><li>SDK</li><li>Shared</li><li>Worker</li><li>Agent</li></ul><p>于是 JetBrains 推出了：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Workspace Mode<br></code></pre></td></tr></table></figure><p>它能自动：</p><ul><li>识别多个子项目</li><li>建立依赖关系</li><li>自动配置 SDK</li><li>自动 linking</li><li>自动处理 import</li></ul><hr><h1 id="六、为什么-uv-是关键"><a href="#六、为什么-uv-是关键" class="headerlink" title="六、为什么 uv 是关键"></a>六、为什么 uv 是关键</h1><p>现在 Python 新生态核心正在变成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">uv<br>+<br>pyproject.toml<br>+<br>workspace<br></code></pre></td></tr></table></figure><p>而不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">requirements.txt<br>setup.py<br>pip<br></code></pre></td></tr></table></figure><p>uv 是 Astral 推出的新一代 Python 包管理工具。</p><p>特点：</p><ul><li>极快</li><li>原生 workspace</li><li>自动 venv</li><li>lockfile</li><li>monorepo 支持优秀</li></ul><hr><h1 id="七、开始实战：创建-Workspace"><a href="#七、开始实战：创建-Workspace" class="headerlink" title="七、开始实战：创建 Workspace"></a>七、开始实战：创建 Workspace</h1><h2 id="1-安装-uv"><a href="#1-安装-uv" class="headerlink" title="1. 安装 uv"></a>1. 安装 uv</h2><p>Mac：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">brew install uv<br></code></pre></td></tr></table></figure><p>Linux &#x2F; Mac：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -LsSf https://astral.sh/uv/install.sh | sh<br></code></pre></td></tr></table></figure><p>检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv --version<br></code></pre></td></tr></table></figure><hr><h2 id="2-创建根目录"><a href="#2-创建根目录" class="headerlink" title="2. 创建根目录"></a>2. 创建根目录</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> my-platform<br><span class="hljs-built_in">cd</span> my-platform<br></code></pre></td></tr></table></figure><p>初始化：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv init<br></code></pre></td></tr></table></figure><hr><h2 id="3-配置-Workspace-Root"><a href="#3-配置-Workspace-Root" class="headerlink" title="3. 配置 Workspace Root"></a>3. 配置 Workspace Root</h2><p>修改根目录：</p><h3 id="pyproject-toml"><a href="#pyproject-toml" class="headerlink" title="pyproject.toml"></a>pyproject.toml</h3><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs toml"><span class="hljs-section">[project]</span><br><span class="hljs-attr">name</span> = <span class="hljs-string">&quot;my-platform&quot;</span><br><span class="hljs-attr">version</span> = <span class="hljs-string">&quot;0.1.0&quot;</span><br><span class="hljs-attr">requires-python</span> = <span class="hljs-string">&quot;&gt;=3.12&quot;</span><br><br><span class="hljs-section">[tool.uv.workspace]</span><br><span class="hljs-attr">members</span> = [<br>    <span class="hljs-string">&quot;apps/*&quot;</span>,<br>    <span class="hljs-string">&quot;libs/*&quot;</span><br>]<br></code></pre></td></tr></table></figure><hr><h1 id="八、创建子项目"><a href="#八、创建子项目" class="headerlink" title="八、创建子项目"></a>八、创建子项目</h1><h2 id="1-创建-API-服务"><a href="#1-创建-API-服务" class="headerlink" title="1. 创建 API 服务"></a>1. 创建 API 服务</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p apps/api<br><span class="hljs-built_in">cd</span> apps/api<br><br>uv init<br></code></pre></td></tr></table></figure><hr><h2 id="2-创建共享库"><a href="#2-创建共享库" class="headerlink" title="2. 创建共享库"></a>2. 创建共享库</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p libs/shared<br><span class="hljs-built_in">cd</span> libs/shared<br><br>uv init --lib<br></code></pre></td></tr></table></figure><p>这里：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">--lib<br></code></pre></td></tr></table></figure><p>表示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">这是一个 library<br></code></pre></td></tr></table></figure><p>而不是 app。</p><hr><h1 id="九、配置-Workspace-内部依赖"><a href="#九、配置-Workspace-内部依赖" class="headerlink" title="九、配置 Workspace 内部依赖"></a>九、配置 Workspace 内部依赖</h1><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">api<br>依赖<br>shared<br></code></pre></td></tr></table></figure><p>那么：</p><h2 id="apps-api-pyproject-toml"><a href="#apps-api-pyproject-toml" class="headerlink" title="apps&#x2F;api&#x2F;pyproject.toml"></a>apps&#x2F;api&#x2F;pyproject.toml</h2><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs toml"><span class="hljs-section">[project]</span><br><span class="hljs-attr">name</span> = <span class="hljs-string">&quot;api&quot;</span><br><span class="hljs-attr">version</span> = <span class="hljs-string">&quot;0.1.0&quot;</span><br><br><span class="hljs-attr">dependencies</span> = [<br>    <span class="hljs-string">&quot;shared&quot;</span><br>]<br><br><span class="hljs-section">[tool.uv.sources]</span><br><span class="hljs-attr">shared</span> = &#123; workspace = <span class="hljs-literal">true</span> &#125;<br></code></pre></td></tr></table></figure><p>这里：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs toml"><span class="hljs-attr">workspace</span> = <span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure><p>是核心。</p><p>意思：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">shared 不从 PyPI 安装<br>而是从当前 workspace 找<br></code></pre></td></tr></table></figure><hr><h1 id="十、统一安装依赖"><a href="#十、统一安装依赖" class="headerlink" title="十、统一安装依赖"></a>十、统一安装依赖</h1><p>回到根目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> my-platform<br></code></pre></td></tr></table></figure><p>执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">sync</span><br></code></pre></td></tr></table></figure><p>或者：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">sync</span> --all-packages<br></code></pre></td></tr></table></figure><p>这会：</p><ul><li>创建统一 .venv</li><li>安装全部依赖</li><li>建立 workspace linking</li><li>生成 uv.lock</li></ul><hr><h1 id="十一、PyCharm-如何配置-Workspace-Mode"><a href="#十一、PyCharm-如何配置-Workspace-Mode" class="headerlink" title="十一、PyCharm 如何配置 Workspace Mode"></a>十一、PyCharm 如何配置 Workspace Mode</h1><p>直接：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">打开整个 my-platform<br></code></pre></td></tr></table></figure><p>不是打开：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">apps/api<br></code></pre></td></tr></table></figure><p>然后：</p><p>PyCharm 会自动检测：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">workspace project detected<br></code></pre></td></tr></table></figure><p>开启：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Enable Workspace Mode<br></code></pre></td></tr></table></figure><p>或者：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">Settings<br>→ Project Structure<br>→ Use pyproject.toml-based project model (Beta)<br></code></pre></td></tr></table></figure><p>开启即可。</p><hr><h1 id="十二、开启后会发生什么"><a href="#十二、开启后会发生什么" class="headerlink" title="十二、开启后会发生什么"></a>十二、开启后会发生什么</h1><p>PyCharm 会自动：</p><h2 id="自动识别所有成员"><a href="#自动识别所有成员" class="headerlink" title="自动识别所有成员"></a>自动识别所有成员</h2><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">apps/api<br>libs/shared<br>libs/rag<br></code></pre></td></tr></table></figure><h2 id="自动处理-import"><a href="#自动处理-import" class="headerlink" title="自动处理 import"></a>自动处理 import</h2><p>以前：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> shared.utils <span class="hljs-keyword">import</span> xxx<br></code></pre></td></tr></table></figure><p>可能飘红。</p><p>现在：</p><p>自动识别。</p><h2 id="自动统一-SDK"><a href="#自动统一-SDK" class="headerlink" title="自动统一 SDK"></a>自动统一 SDK</h2><p>整个 workspace 共用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">.venv<br></code></pre></td></tr></table></figure><p>不需要手动切换 interpreter。</p><hr><h1 id="十三、部署怎么办？"><a href="#十三、部署怎么办？" class="headerlink" title="十三、部署怎么办？"></a>十三、部署怎么办？</h1><p>很多人会问：</p><blockquote><p>“如果 API 依赖 shared，<br>那部署 API 时不是整个 monorepo 都要部署？”</p></blockquote><p>答案：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">不是<br></code></pre></td></tr></table></figure><hr><h1 id="十四、要区分两种东西"><a href="#十四、要区分两种东西" class="headerlink" title="十四、要区分两种东西"></a>十四、要区分两种东西</h1><h2 id="1-代码级依赖"><a href="#1-代码级依赖" class="headerlink" title="1. 代码级依赖"></a>1. 代码级依赖</h2><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">shared/<br>rag/<br>llm_core/<br></code></pre></td></tr></table></figure><p>这些本来就应该跟随 API。</p><p>因为：</p><p>API 运行需要它们。</p><hr><h2 id="2-服务级依赖"><a href="#2-服务级依赖" class="headerlink" title="2. 服务级依赖"></a>2. 服务级依赖</h2><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">worker/<br>crawler/<br>embedding_service/<br></code></pre></td></tr></table></figure><p>这些不应该直接 import。</p><p>应该：</p><ul><li>HTTP</li><li>gRPC</li><li>MQ</li><li>Redis Queue</li></ul><p>通信。</p><hr><h1 id="十五、正确的-Monorepo-架构"><a href="#十五、正确的-Monorepo-架构" class="headerlink" title="十五、正确的 Monorepo 架构"></a>十五、正确的 Monorepo 架构</h1><p>推荐：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs text">my-platform/<br>├── apps/<br>│   ├── api<br>│   ├── worker<br>│   ├── crawler<br>│   └── mcp_server<br>│<br>├── libs/<br>│   ├── shared<br>│   ├── rag<br>│   ├── llm_core<br>│   └── ai_utils<br></code></pre></td></tr></table></figure><p>规则：</p><h2 id="apps"><a href="#apps" class="headerlink" title="apps"></a>apps</h2><p>独立服务。</p><p>不要互相 import。</p><h2 id="libs"><a href="#libs" class="headerlink" title="libs"></a>libs</h2><p>共享代码。</p><p>允许 workspace dependency。</p><hr><h1 id="十六、Docker-如何部署"><a href="#十六、Docker-如何部署" class="headerlink" title="十六、Docker 如何部署"></a>十六、Docker 如何部署</h1><p>例如：</p><p>API Dockerfile：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.12</span><br><br><span class="hljs-keyword">WORKDIR</span><span class="language-bash"> /app</span><br><br><span class="hljs-keyword">COPY</span><span class="language-bash"> . .</span><br><br><span class="hljs-keyword">RUN</span><span class="language-bash"> uv <span class="hljs-built_in">sync</span> --package api</span><br><br><span class="hljs-keyword">CMD</span><span class="language-bash"> [<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;-m&quot;</span>, <span class="hljs-string">&quot;api&quot;</span>]</span><br></code></pre></td></tr></table></figure><p>这里：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">sync</span> --package api<br></code></pre></td></tr></table></figure><p>只会安装：</p><ul><li>api</li><li>api 的 workspace 依赖</li></ul><p>不会安装整个 monorepo。</p><hr><h1 id="十七、推荐的一套现代-Python-技术栈"><a href="#十七、推荐的一套现代-Python-技术栈" class="headerlink" title="十七、推荐的一套现代 Python 技术栈"></a>十七、推荐的一套现代 Python 技术栈</h1><p>当前非常推荐：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs text">uv<br>+ workspace<br>+ pyproject.toml<br>+ ruff<br>+ pytest<br>+ PyCharm Workspace Mode<br></code></pre></td></tr></table></figure><p>这是目前 Python 新工程体系里，<br>最接近现代化大型工程的一种方案。</p><hr><h1 id="十八、总结"><a href="#十八、总结" class="headerlink" title="十八、总结"></a>十八、总结</h1><p>PyCharm Workspace Mode 的核心不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">“多开几个目录”<br></code></pre></td></tr></table></figure><p>而是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Python 开始正式进入 Monorepo 时代<br></code></pre></td></tr></table></figure><p>而 uv Workspace：</p><p>则是这个生态的真正基础。</p><p>未来几年：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">uv + workspace<br></code></pre></td></tr></table></figure><p>大概率会逐渐成为：</p><p>大型 Python 项目的标准组织方式。</p>]]>
    </content>
    <id>https://yuluod.github.io/2026/05/06/PyCharm-Workspace-Mode-uv-Workspace-%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97/</id>
    <link href="https://yuluod.github.io/2026/05/06/PyCharm-Workspace-Mode-uv-Workspace-%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97/"/>
    <published>2026-05-06T02:53:08.000Z</published>
    <summary>
      <![CDATA[<h2 id="Python-新一代-Monorepo-工程组织方式"><a href="#Python-新一代-Monorepo-工程组织方式" class="headerlink" title="Python 新一代 Monorepo 工程组织方式"></a>Python 新一代 Monorepo 工程组织方式</h2><hr>
<h1 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h1><p>最近打开 PyCharm，你可能会看到这样一个提示：</p>
<figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">Enable and try workspace mode Beta<br><br>A large project can be split into smaller parts<br>(like backend, API, and shared code),<br>making it easier to organize and maintain.<br></code></pre></td></tr></table></figure>

<p>很多人第一反应：</p>
<blockquote>
<p>“这是啥？”</p>
<p>“和普通 Project 有什么区别？”</p>
<p>“是不是类似 pnpm workspace？”</p>
<p>“Python 终于开始搞 monorepo 了吗？”</p>
</blockquote>
<p>实际上：</p>
<p>这个功能背后，代表的是 Python 工程组织方式的一次重大升级。</p>
<p>而它真正对应的生态，其实是：</p>
<figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">uv + pyproject.toml + workspace<br></code></pre></td></tr></table></figure>

<p>这篇文章会从：</p>
<ul>
<li>为什么需要 Workspace</li>
<li>什么是 Monorepo</li>
<li>uv Workspace 怎么用</li>
<li>PyCharm Workspace Mode 如何配合</li>
<li>实际部署怎么做</li>
<li>如何避免架构混乱</li>
</ul>
<p>完整讲清楚。</p>]]>
    </summary>
    <title>PyCharm Workspace Mode + uv Workspace 实战指南</title>
    <updated>2026-05-06T02:54:11.140Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="GitHub" scheme="https://yuluod.github.io/categories/GitHub/"/>
    <category term="GitHub Actions" scheme="https://yuluod.github.io/tags/GitHub-Actions/"/>
    <content>
      <![CDATA[<p>最近踩了个挺典型的坑：<br>前一天还能正常构建，第二天终端里 <code>cargo --version</code> 直接变成 <code>command not found</code>。更离谱的是，<code>~/.zshrc</code> 里 PATH 配置明明还在。</p><p>当时第一反应是 shell 配置坏了，结果查下来不是。</p><span id="more"></span><h2 id="现象看起来像-PATH，实际上是“断链”"><a href="#现象看起来像-PATH，实际上是“断链”" class="headerlink" title="现象看起来像 PATH，实际上是“断链”"></a>现象看起来像 PATH，实际上是“断链”</h2><p>先看几个关键信息：</p><ul><li><code>PATH</code> 里有 <code>~/.cargo/bin</code></li><li><code>~/.cargo/bin/cargo</code> 存在</li><li>但 <code>cargo</code> 是个软链：<code>cargo -&gt; rustup</code></li><li>真正的 <code>~/.cargo/bin/rustup</code> 文件没了</li></ul><p>也就是说，不是找不到 <code>cargo</code>，是 <code>cargo</code> 指向的目标没了。</p><p>这种情况很容易误判成“环境变量没生效”，其实是工具链启动器被清理了。</p><h2 id="根因：self-hosted-runner-和开发环境共用-HOME，被-CI-的缓存清理逻辑误伤"><a href="#根因：self-hosted-runner-和开发环境共用-HOME，被-CI-的缓存清理逻辑误伤" class="headerlink" title="根因：self-hosted runner 和开发环境共用 HOME，被 CI 的缓存清理逻辑误伤"></a>根因：self-hosted runner 和开发环境共用 HOME，被 CI 的缓存清理逻辑误伤</h2><p>我项目是 GitHub Actions 跑在本机 self-hosted runner 上（同一个用户）。<br>Workflow 里用了 <code>swatinem/rust-cache@v2</code>，默认 <code>cache-bin: true</code>。</p><p>这件事在 GitHub-hosted runner 上通常没问题，因为机器是一次性的；<br>但在长期运行的 self-hosted 上，<code>~/.cargo/bin</code> 是你本机常驻环境的一部分，一旦 post-cleanup 动到它，就会出现这种“本机 cargo 消失”的事故。</p><p>一句话总结：<strong>CI 没删你代码，但删了你本机共享环境里的关键二进制</strong>。</p><h2 id="最小改动修复"><a href="#最小改动修复" class="headerlink" title="最小改动修复"></a>最小改动修复</h2><p>我最后没把缓存全关，而是只关了 bin 缓存：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cache</span> <span class="hljs-string">Rust</span> <span class="hljs-string">build</span><br>  <span class="hljs-attr">uses:</span> <span class="hljs-string">swatinem/rust-cache@v2</span><br>  <span class="hljs-attr">with:</span><br>    <span class="hljs-attr">workspaces:</span> <span class="hljs-string">&#x27;./src-tauri -&gt; target&#x27;</span><br>    <span class="hljs-attr">cache-bin:</span> <span class="hljs-string">&quot;false&quot;</span><br></code></pre></td></tr></table></figure><p>这行建议至少加在所有 self-hosted 的 Rust job 里（比如 macOS &#x2F; Linux）。<br>这样还能继续缓存 registry&#x2F;git&#x2F;target，构建速度保住，同时避开 <code>~/.cargo/bin</code> 被清理。</p><h2 id="如果你现在已经中招了，可以先这么做"><a href="#如果你现在已经中招了，可以先这么做" class="headerlink" title="如果你现在已经中招了，可以先这么做"></a>如果你现在已经中招了，可以先这么做</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl https://sh.rustup.rs -sSf | sh -s -- -y<br><span class="hljs-built_in">source</span> <span class="hljs-string">&quot;<span class="hljs-variable">$HOME</span>/.cargo/env&quot;</span><br>cargo --version<br>rustup show<br></code></pre></td></tr></table></figure><p>另外建议顺手检查一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span> -l ~/.cargo/bin/cargo<br><span class="hljs-built_in">ls</span> -l ~/.cargo/bin/rustup<br></code></pre></td></tr></table></figure><p>如果 <code>cargo -&gt; rustup</code> 但 rustup 不存在，就是同类问题。</p><h2 id="这类问题的通用治理建议（不只-Rust）"><a href="#这类问题的通用治理建议（不只-Rust）" class="headerlink" title="这类问题的通用治理建议（不只 Rust）"></a>这类问题的通用治理建议（不只 Rust）</h2><p>这次是 Rust，换成 Node&#x2F;Python&#x2F;Go 也可能发生。核心原则就两条：</p><ol><li><p><strong>不要让 CI 和你的日常开发环境“共用生活空间”</strong><br>最好给 runner 单独用户（甚至单独机器&#x2F;容器）。</p></li><li><p><strong>缓存“构建产物”和“依赖仓库”，少碰“全局工具目录”</strong><br>例如 <code>~/.cargo/bin</code>、<code>~/.nvm</code>、<code>~/.pyenv</code> 这种位置，一旦被清理就是系统性故障。</p></li></ol><p>再补一个实用建议：<br>给 self-hosted runner 加个构建前自检脚本（比如检查 <code>cargo/node/python</code> 是否可用），早失败比跑完才发现要省很多时间。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>self-hosted 的优势是快、可控、省钱；<br>代价是你得把“本机”当“生产环境”来管理。</p><p>这次问题本身不复杂，但很有代表性：<br><strong>不是代码错了，是运行边界没隔离好。</strong></p><p>如果你也在用 GitHub Actions + self-hosted，建议现在就看一眼你的 workflow：<br>缓存到底是在加速构建，还是在悄悄改你的机器状态。</p>]]>
    </content>
    <id>https://yuluod.github.io/2026/02/09/%E4%B8%80%E6%AC%A1%E3%80%8Ccargo-%E7%AA%81%E7%84%B6%E6%B6%88%E5%A4%B1%E3%80%8D%E4%B9%8B%E5%90%8E%EF%BC%9A%E8%81%8A%E8%81%8A-GitHub-Actions-%E6%9C%AC%E6%9C%BA-self-hosted-runner-%E7%9A%84%E6%AD%A3%E7%A1%AE%E6%89%93%E5%BC%80%E6%96%B9%E5%BC%8F/</id>
    <link href="https://yuluod.github.io/2026/02/09/%E4%B8%80%E6%AC%A1%E3%80%8Ccargo-%E7%AA%81%E7%84%B6%E6%B6%88%E5%A4%B1%E3%80%8D%E4%B9%8B%E5%90%8E%EF%BC%9A%E8%81%8A%E8%81%8A-GitHub-Actions-%E6%9C%AC%E6%9C%BA-self-hosted-runner-%E7%9A%84%E6%AD%A3%E7%A1%AE%E6%89%93%E5%BC%80%E6%96%B9%E5%BC%8F/"/>
    <published>2026-02-09T14:40:36.000Z</published>
    <summary>
      <![CDATA[<p>最近踩了个挺典型的坑：<br>前一天还能正常构建，第二天终端里 <code>cargo --version</code> 直接变成 <code>command not found</code>。更离谱的是，<code>~/.zshrc</code> 里 PATH 配置明明还在。</p>
<p>当时第一反应是 shell 配置坏了，结果查下来不是。</p>]]>
    </summary>
    <title>一次「cargo 突然消失」之后：聊聊 GitHub Actions + 本机 self-hosted runner 的正确打开方式</title>
    <updated>2026-02-10T06:46:52.894Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Cloudflare" scheme="https://yuluod.github.io/categories/Cloudflare/"/>
    <category term="Cloudflare" scheme="https://yuluod.github.io/tags/Cloudflare/"/>
    <content>
      <![CDATA[<blockquote><p>本文记录了在实际项目中使用 Cloudflare R2 + Workers 构建软件下载分发体系时，从最初设计、踩坑、排错，到最终稳定方案的完整过程。内容包含 <strong>Cache Rules 与 Worker 缓存冲突的真实问题</strong>，适合直接作为生产级参考。</p></blockquote><hr><h2 id="一、背景与目标"><a href="#一、背景与目标" class="headerlink" title="一、背景与目标"></a>一、背景与目标</h2><p>在为 <strong>AirTools</strong> 构建桌面端分发体系时，我希望实现：</p><ul><li>安装包（<code>.exe</code> &#x2F; <code>.dmg</code> 等）全球 CDN 强缓存</li><li>版本元信息（<code>latest.json</code>）实时更新、不被缓存</li><li>不自建服务器，尽量使用 Cloudflare 原生能力</li><li>缓存行为稳定、可预测，不出现随机 500</li></ul><p>最终技术选型为 <strong>Cloudflare R2 + Cloudflare Workers</strong>。</p><hr><span id="more"></span><h2 id="二、初始架构设计"><a href="#二、初始架构设计" class="headerlink" title="二、初始架构设计"></a>二、初始架构设计</h2><p>目标结构如下：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-attribute">Client</span><br>  ↓<br>Cloudflare Edge<br>  ├── latest.json → Worker → R2（不缓存）<br>  └── <span class="hljs-regexp">*.exe</span>/<span class="hljs-regexp">*.dmg</span> → Worker → R2 → Edge Cache（强缓存）<br></code></pre></td></tr></table></figure><p>逻辑上并不复杂，但在实际落地过程中，问题主要集中在 <strong>缓存控制细节</strong> 上。</p><hr><h2 id="三、最早遇到的异常现象"><a href="#三、最早遇到的异常现象" class="headerlink" title="三、最早遇到的异常现象"></a>三、最早遇到的异常现象</h2><h3 id="1️⃣-缓存始终不生效"><a href="#1️⃣-缓存始终不生效" class="headerlink" title="1️⃣ 缓存始终不生效"></a>1️⃣ 缓存始终不生效</h3><p>即便返回了：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs http"><span class="hljs-attribute">Cache-Control</span><span class="hljs-punctuation">: </span>public, max-age=2592000, immutable<br></code></pre></td></tr></table></figure><p>响应中仍反复出现：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs http"><span class="hljs-attribute">cf-cache-status</span><span class="hljs-punctuation">: </span>DYNAMIC<br></code></pre></td></tr></table></figure><hr><h3 id="2️⃣-同一个-URL，返回结果不一致"><a href="#2️⃣-同一个-URL，返回结果不一致" class="headerlink" title="2️⃣ 同一个 URL，返回结果不一致"></a>2️⃣ 同一个 URL，返回结果不一致</h3><p>对同一下载地址多次执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -I https://dl.airtools.app/v0.1.19/AirTools_0.1.19_x64-setup.exe<br></code></pre></td></tr></table></figure><p>会出现：</p><ul><li>有时 <code>200</code></li><li>有时 <code>500</code></li><li>缓存状态随机变化</li></ul><p>这在下载分发场景中是非常危险的信号。</p><hr><h3 id="3️⃣-Cloudflare-Dashboard-偶发-502"><a href="#3️⃣-Cloudflare-Dashboard-偶发-502" class="headerlink" title="3️⃣ Cloudflare Dashboard 偶发 502"></a>3️⃣ Cloudflare Dashboard 偶发 502</h3><p>在 Workers &#x2F; Builds 页面中，Cloudflare 控制台本身也开始报：</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">API</span> Request Failed (<span class="hljs-number">502</span>)<br></code></pre></td></tr></table></figure><p>这通常意味着某个 Worker 在边缘节点频繁返回 5xx。</p><hr><h2 id="四、Cache-Rules-带来的隐藏问题（关键）"><a href="#四、Cache-Rules-带来的隐藏问题（关键）" class="headerlink" title="四、Cache Rules 带来的隐藏问题（关键）"></a>四、Cache Rules 带来的隐藏问题（关键）</h2><p>在排查过程中，一个非常容易被忽略的问题逐渐浮现：<strong>Cache Rules 与 Worker 的缓存逻辑发生了冲突</strong>。</p><h3 id="1️⃣-Cache-Rules-的“误导性描述”"><a href="#1️⃣-Cache-Rules-的“误导性描述”" class="headerlink" title="1️⃣ Cache Rules 的“误导性描述”"></a>1️⃣ Cache Rules 的“误导性描述”</h3><p>Cloudflare Cache Rules 中常见的选项包括：</p><ul><li>缓存资格（Eligible for cache）</li><li>绕过缓存（Bypass cache）</li></ul><p>这些规则的描述很容易让人误以为：</p><blockquote><p>“只要标记为符合缓存条件，Cloudflare 就一定会缓存”</p></blockquote><p><strong>这是不准确的。</strong></p><p>Cache Rules 只是在「是否允许缓存」这一层做判断，<strong>真正是否缓存，仍然取决于：</strong></p><ul><li>Worker 中是否使用 <code>cacheEverything</code></li><li>返回的 HTTP 状态码是否为 200</li><li>实际响应头中的 <code>Cache-Control</code></li></ul><hr><h3 id="2️⃣-Cache-Rules-覆盖-Worker-Header"><a href="#2️⃣-Cache-Rules-覆盖-Worker-Header" class="headerlink" title="2️⃣ Cache Rules 覆盖 Worker Header"></a>2️⃣ Cache Rules 覆盖 Worker Header</h3><p>在早期配置中，我曾在 Cache Rules 中尝试：</p><ul><li>针对 <code>.exe / .dmg</code> 设置缓存</li><li>或统一修改 <code>Cache-Control</code></li></ul><p>结果是：</p><ul><li>Worker 返回的 <code>Cache-Control</code> 被规则覆盖</li><li>错误响应（500）也被错误地标记为可缓存候选</li><li>实际缓存行为变得不可预测</li></ul><p><strong>结论：当请求经过 Worker 时，Cache Rules 很容易成为“干扰项”。</strong></p><hr><h3 id="3️⃣-最终选择：让-Worker-成为唯一缓存决策点"><a href="#3️⃣-最终选择：让-Worker-成为唯一缓存决策点" class="headerlink" title="3️⃣ 最终选择：让 Worker 成为唯一缓存决策点"></a>3️⃣ 最终选择：让 Worker 成为唯一缓存决策点</h3><p>在最终方案中：</p><ul><li><strong>不再依赖 Cache Rules 控制下载缓存</strong></li><li>Cache Rules 保持最小化甚至删除</li><li>所有缓存判断统一在 Worker 中完成</li></ul><p>这样可以保证：</p><blockquote><p>缓存逻辑是可读的、可调试的、可预测的。</p></blockquote><hr><h2 id="五、真正的根因总结"><a href="#五、真正的根因总结" class="headerlink" title="五、真正的根因总结"></a>五、真正的根因总结</h2><p>综合排查后，问题根因集中在三点：</p><ol><li>Worker 未处理回源失败，导致 500 被返回</li><li>500 响应参与缓存资格判断，污染后续请求</li><li>Cache Rules 与 Worker 缓存逻辑发生冲突</li></ol><hr><h2 id="六、最终稳定方案设计"><a href="#六、最终稳定方案设计" class="headerlink" title="六、最终稳定方案设计"></a>六、最终稳定方案设计</h2><p>核心原则只有一句话：</p><blockquote><p><strong>只缓存成功的安装包，失败请求永不进入缓存路径</strong></p></blockquote><h3 id="缓存策略表"><a href="#缓存策略表" class="headerlink" title="缓存策略表"></a>缓存策略表</h3><table><thead><tr><th>类型</th><th>是否缓存</th><th>说明</th></tr></thead><tbody><tr><td><code>.exe / .dmg / .zip</code></td><td>✅</td><td>强缓存 30 天</td></tr><tr><td><code>latest.json</code></td><td>❌</td><td>永远回源</td></tr><tr><td>500 &#x2F; 502</td><td>❌</td><td>禁止缓存</td></tr></tbody></table><hr><h2 id="七、最终-Worker-实现（生产可用）"><a href="#七、最终-Worker-实现（生产可用）" class="headerlink" title="七、最终 Worker 实现（生产可用）"></a>七、最终 Worker 实现（生产可用）</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> &#123;<br>  <span class="hljs-keyword">async</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-params">request, env, ctx</span>) &#123;<br>    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(request.<span class="hljs-property">url</span>)<br>    <span class="hljs-keyword">const</span> path = url.<span class="hljs-property">pathname</span><br><br>    <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">ORIGIN</span> = <span class="hljs-string">&#x27;https://dl.airtools.app&#x27;</span><br>    <span class="hljs-keyword">const</span> originUrl = <span class="hljs-variable constant_">ORIGIN</span> + path + url.<span class="hljs-property">search</span><br><br>    <span class="hljs-keyword">if</span> (path.<span class="hljs-title function_">endsWith</span>(<span class="hljs-string">&#x27;/latest.json&#x27;</span>)) &#123;<br>      <span class="hljs-keyword">let</span> res<br>      <span class="hljs-keyword">try</span> &#123;<br>        res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(originUrl, &#123; <span class="hljs-attr">cf</span>: &#123; <span class="hljs-attr">cacheEverything</span>: <span class="hljs-literal">false</span> &#125; &#125;)<br>      &#125; <span class="hljs-keyword">catch</span> &#123;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">&#x27;Upstream error&#x27;</span>, &#123; <span class="hljs-attr">status</span>: <span class="hljs-number">502</span> &#125;)<br>      &#125;<br><br>      <span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Headers</span>(res.<span class="hljs-property">headers</span>)<br>      headers.<span class="hljs-title function_">set</span>(<span class="hljs-string">&#x27;Cache-Control&#x27;</span>, <span class="hljs-string">&#x27;no-store, no-cache, must-revalidate&#x27;</span>)<br><br>      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(res.<span class="hljs-property">body</span>, &#123; <span class="hljs-attr">status</span>: res.<span class="hljs-property">status</span>, headers &#125;)<br>    &#125;<br><br>    <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">CACHEABLE_EXT</span> = [<br>      <span class="hljs-string">&#x27;.dmg&#x27;</span>, <span class="hljs-string">&#x27;.exe&#x27;</span>, <span class="hljs-string">&#x27;.msi&#x27;</span>, <span class="hljs-string">&#x27;.AppImage&#x27;</span>,<br>      <span class="hljs-string">&#x27;.deb&#x27;</span>, <span class="hljs-string">&#x27;.rpm&#x27;</span>, <span class="hljs-string">&#x27;.zip&#x27;</span>, <span class="hljs-string">&#x27;.tar.gz&#x27;</span>,<br>      <span class="hljs-string">&#x27;.sha256&#x27;</span>, <span class="hljs-string">&#x27;.sig&#x27;</span><br>    ]<br><br>    <span class="hljs-keyword">const</span> isCacheable = <span class="hljs-variable constant_">CACHEABLE_EXT</span>.<span class="hljs-title function_">some</span>(<span class="hljs-function"><span class="hljs-params">ext</span> =&gt;</span><br>      path.<span class="hljs-title function_">toLowerCase</span>().<span class="hljs-title function_">endsWith</span>(ext)<br>    )<br><br>    <span class="hljs-keyword">let</span> res<br>    <span class="hljs-keyword">try</span> &#123;<br>      res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(originUrl, &#123;<br>        <span class="hljs-attr">cf</span>: &#123;<br>          <span class="hljs-attr">cacheEverything</span>: isCacheable,<br>          <span class="hljs-attr">cacheTtl</span>: isCacheable ? <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span> * <span class="hljs-number">30</span> : <span class="hljs-literal">undefined</span><br>        &#125;<br>      &#125;)<br>    &#125; <span class="hljs-keyword">catch</span> &#123;<br>      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">&#x27;Upstream fetch failed&#x27;</span>, &#123;<br>        <span class="hljs-attr">status</span>: <span class="hljs-number">502</span>,<br>        <span class="hljs-attr">headers</span>: &#123; <span class="hljs-string">&#x27;Cache-Control&#x27;</span>: <span class="hljs-string">&#x27;no-store&#x27;</span> &#125;<br>      &#125;)<br>    &#125;<br><br>    <span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Headers</span>(res.<span class="hljs-property">headers</span>)<br><br>    <span class="hljs-keyword">if</span> (isCacheable &amp;&amp; res.<span class="hljs-property">status</span> === <span class="hljs-number">200</span>) &#123;<br>      headers.<span class="hljs-title function_">set</span>(<span class="hljs-string">&#x27;Cache-Control&#x27;</span>, <span class="hljs-string">&#x27;public, max-age=2592000, immutable&#x27;</span>)<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>      headers.<span class="hljs-title function_">set</span>(<span class="hljs-string">&#x27;Cache-Control&#x27;</span>, <span class="hljs-string">&#x27;no-store, no-cache&#x27;</span>)<br>    &#125;<br><br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(res.<span class="hljs-property">body</span>, &#123; <span class="hljs-attr">status</span>: res.<span class="hljs-property">status</span>, headers &#125;)<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><hr><h2 id="八、最终验证结果"><a href="#八、最终验证结果" class="headerlink" title="八、最终验证结果"></a>八、最终验证结果</h2><ul><li><p><code>latest.json</code></p><ul><li><code>cf-cache-status: DYNAMIC</code></li><li>实时回源</li></ul></li><li><p>安装包下载</p><ul><li><code>cf-cache-status: HIT</code></li><li><code>age</code> 持续增长</li><li>支持断点续传</li></ul></li></ul><hr><h2 id="九、经验总结"><a href="#九、经验总结" class="headerlink" title="九、经验总结"></a>九、经验总结</h2><ol><li>不要缓存失败响应</li><li>Worker 必须处理 <code>fetch</code> 异常</li><li>Cache Rules 不适合与复杂 Worker 缓存逻辑混用</li><li>一个域名只承担一种角色（Worker 或 R2）</li><li>判断缓存是否生效，以 <code>cf-cache-status</code> 为准</li></ol><hr><h2 id="十、结语"><a href="#十、结语" class="headerlink" title="十、结语"></a>十、结语</h2><p>Cloudflare R2 + Workers 非常适合用于软件下载分发，但<strong>稳定性来自于细节，而不是默认配置</strong>。</p><p>只要把缓存决策收敛到 Worker 内部，并明确区分“可缓存内容”和“实时内容”，这套架构可以长期稳定运行，且维护成本极低。</p>]]>
    </content>
    <id>https://yuluod.github.io/2026/01/29/%E4%BD%BF%E7%94%A8-Cloudflare-R2-Workers-%E6%9E%84%E5%BB%BA%E7%A8%B3%E5%AE%9A%E3%80%81%E9%AB%98%E6%80%A7%E8%83%BD%E7%9A%84%E8%BD%AF%E4%BB%B6%E4%B8%8B%E8%BD%BD%E5%88%86%E5%8F%91/</id>
    <link href="https://yuluod.github.io/2026/01/29/%E4%BD%BF%E7%94%A8-Cloudflare-R2-Workers-%E6%9E%84%E5%BB%BA%E7%A8%B3%E5%AE%9A%E3%80%81%E9%AB%98%E6%80%A7%E8%83%BD%E7%9A%84%E8%BD%AF%E4%BB%B6%E4%B8%8B%E8%BD%BD%E5%88%86%E5%8F%91/"/>
    <published>2026-01-29T03:05:14.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>本文记录了在实际项目中使用 Cloudflare R2 + Workers 构建软件下载分发体系时，从最初设计、踩坑、排错，到最终稳定方案的完整过程。内容包含 <strong>Cache Rules 与 Worker 缓存冲突的真实问题</strong>，适合直接作为生产级参考。</p>
</blockquote>
<hr>
<h2 id="一、背景与目标"><a href="#一、背景与目标" class="headerlink" title="一、背景与目标"></a>一、背景与目标</h2><p>在为 <strong>AirTools</strong> 构建桌面端分发体系时，我希望实现：</p>
<ul>
<li>安装包（<code>.exe</code> &#x2F; <code>.dmg</code> 等）全球 CDN 强缓存</li>
<li>版本元信息（<code>latest.json</code>）实时更新、不被缓存</li>
<li>不自建服务器，尽量使用 Cloudflare 原生能力</li>
<li>缓存行为稳定、可预测，不出现随机 500</li>
</ul>
<p>最终技术选型为 <strong>Cloudflare R2 + Cloudflare Workers</strong>。</p>
<hr>]]>
    </summary>
    <title>使用 Cloudflare R2 + Workers 构建稳定、高性能的软件下载分发</title>
    <updated>2026-01-29T03:06:35.275Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Front-Dev" scheme="https://yuluod.github.io/categories/Front-Dev/"/>
    <category term="oklch," scheme="https://yuluod.github.io/tags/oklch/"/>
    <content>
      <![CDATA[<p>在做自己的项目时，我逐渐发现一个问题：<br>颜色越写越乱、越调越脏，很多看似简单的调色逻辑在实际效果上完全不符合预期。</p><p>比如：<br>    •明明只是想调亮一点，但 RGB&#x2F;HSL 调完后颜色不是变灰就是变艳；<br>    •做渐变时颜色中途莫名其妙发暗；<br>    •图表配色在亮色主题和暗色主题下反差不一致；<br>    •在宽色域（如 P3）设备上，颜色表现和预期差得更远。</p><p>这些都指向同一个底层原因：<br>RGB（甚至 HSL&#x2F;HSV）并不是一个符合人眼感知的色彩空间。</p><p>于是我开始把新的颜色计算逻辑逐步迁移到 OKLCH —— 一个感知均匀、现代化、已被浏览器原生支持的色彩空间。<br>这篇文章来分享我为什么做这个选择，也希望给你在项目色彩处理上的参考。</p><span id="more"></span><h2 id="一、RGB-HSL-在实际开发中有哪些明显痛点？"><a href="#一、RGB-HSL-在实际开发中有哪些明显痛点？" class="headerlink" title="一、RGB &#x2F; HSL 在实际开发中有哪些明显痛点？"></a>一、RGB &#x2F; HSL 在实际开发中有哪些明显痛点？</h2><ol><li>RGB 不是为调色设计的</li></ol><p>RGB 是硬件色彩空间，核心目的是给屏幕发光，不是为了“符合人眼感知”。</p><p>所以你在 RGB 里调亮度，实际上是同时改三个通道，结果往往是“变亮但不自然”。</p><ol start="2"><li>HSL&#x2F;HSV 看似更直观，但本质还是数学假设</li></ol><p>HSL 的亮度是一个简单公式：<br>L &#x3D; (max + min) &#x2F; 2<br>这与人眼的亮暗感受毫无关系。</p><p>所以它经常出现：<br>    •提亮颜色 → 变灰<br>    •调饱和度 → 色相漂移<br>    •淡色系 → 明显“脏”</p><p>前端工程师、设计师乃至最近的 AI 图像后处理都会遇到这类问题。</p><p>⸻</p><h2 id="二、为什么-OKLCH-是更适合现代开发的替代方案？"><a href="#二、为什么-OKLCH-是更适合现代开发的替代方案？" class="headerlink" title="二、为什么 OKLCH 是更适合现代开发的替代方案？"></a>二、为什么 OKLCH 是更适合现代开发的替代方案？</h2><p>OKLCH 属于 OKLab 色彩模型的极简表示形式，结构非常简单：<br>    •O：感知亮度<br>    •K：色度（饱和度）<br>    •LCH：和 CIELAB 类似的极坐标表示</p><p>它的关键价值可以用一句话总结：</p><p>OKLCH 更接近人眼真实的颜色感知，是为现代宽色域、高精度显示时代而设计的。</p><p>⸻</p><h2 id="三、实际使用中的-5-个核心优势"><a href="#三、实际使用中的-5-个核心优势" class="headerlink" title="三、实际使用中的 5 个核心优势"></a>三、实际使用中的 5 个核心优势</h2><ol><li>调色结果更“像人眼认知”</li></ol><p>在 OKLCH：<br>    •调亮 → 真的变亮，不变灰<br>    •调饱和度 → 不会突然偏色<br>    •改色相 → 渐变更自然、平滑</p><p>只要你写过配色算法，就会立刻感觉到这点的巨大差异。</p><p>⸻</p><ol start="2"><li>渐变、插值更自然</li></ol><p>RGB 做渐变常常中间变脏、发黑；<br>HSL 做渐变色相容易发生折返、跳跃。</p><p>OKLCH 解决了这些：<br>    •渐变更线性更均匀<br>    •不会突然暗掉<br>    •更接近真实光学过渡</p><p>这点在图表、数据可视化、背景渐变中非常实用。</p><p>⸻</p><ol start="3"><li>在宽色域（P3 &#x2F; Rec.2020）下表现稳定</li></ol><p>很多新设备（Mac、iPhone、现代安卓机）已经进入了 P3 时代，RGB&#x2F;HSL 的效果在宽色域中更不稳定。</p><p>OKLCH 是为这类显示技术设计的：<br>    •高饱和区域不塌陷<br>    •色彩更加一致<br>    •HDR 适配更稳定</p><p>对未来兼容性非常友好。</p><p>⸻</p><ol start="4"><li>CSS 已经原生支持，迁移成本极低</li></ol><p>现在浏览器直接支持：</p><p>color: oklch(75% 0.15 240);</p><p>不需要 polyfill，也不需要额外库。<br>迁移甚至可以按需局部替换，成本几乎为零。</p><p>⸻</p><ol start="5"><li>更适合自动化配色、可视化、主题系统</li></ol><p>如果你的项目涉及：<br>    •自动生成颜色<br>    •数据可视化（色板、热力图）<br>    •深浅主题适配<br>    •色彩算法<br>    •图像后处理</p><p>OKLCH 让这些变得更简单且结果更稳定。</p><p>举个例子：<br>颜色对比度（contrast）可以直接依赖 OKLab 的亮度计算，比 WCAG 的相对亮度公式更准确，也更符合人眼实际观感。</p><p>⸻</p><h2 id="四、我在自己的项目中是如何使用-OKLCH-的？"><a href="#四、我在自己的项目中是如何使用-OKLCH-的？" class="headerlink" title="四、我在自己的项目中是如何使用 OKLCH 的？"></a>四、我在自己的项目中是如何使用 OKLCH 的？</h2><p>我的做法是 从局部渐进迁移：<br>    1.先从需要自动计算的颜色入手（错误提示色、图表颜色、背景色等）<br>    2.再把主题色、渐变色统一写成 OKLCH<br>    3.最后再做一些视觉微调，确保在亮&#x2F;暗主题都呈现一致对比度</p><p>这种方式不需要一次性重构，几乎没有成本压力。</p><p>⸻</p><h2 id="五、未来为什么需要-OKLCH？"><a href="#五、未来为什么需要-OKLCH？" class="headerlink" title="五、未来为什么需要 OKLCH？"></a>五、未来为什么需要 OKLCH？</h2><p>一句话：</p><p>未来的显示设备越来越宽色域、越来越精细，RGB&#x2F;HSL 已经无法满足现代视觉需求。</p><p>随着 P3 &#x2F; HDR 成为主流，颜色系统的基础越靠近视觉生理模型，效果越稳定。<br>OKLCH 现在能做的事，RGB&#x2F;HSL 越来越做不到。</p><p>⸻</p><h2 id="六、总结：为什么值得迁移？"><a href="#六、总结：为什么值得迁移？" class="headerlink" title="六、总结：为什么值得迁移？"></a>六、总结：为什么值得迁移？</h2><p>有以下几点<br>    •提高颜色质量的成本极低<br>    •代码更清晰，更接近“亮度&#x2F;饱和度&#x2F;色相”的真实含义<br>    •渐变、配色、可视化效果更自然<br>    •兼容性好、未来可扩展<br>    •对项目整体视觉质量有巨大提升</p><p>对我来说，迁移到 OKLCH 是一种 小投入、高收益 的技术决策。</p>]]>
    </content>
    <id>https://yuluod.github.io/2025/11/23/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E5%BC%80%E5%A7%8B%E4%BB%8E-RGB-%E8%BD%AC%E5%90%91-OKLCH-%E2%80%94%E2%80%94-%E9%A2%9C%E8%89%B2%E5%A4%84%E7%90%86%E7%9A%84%E6%96%B0%E9%98%B6%E6%AE%B5/</id>
    <link href="https://yuluod.github.io/2025/11/23/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E5%BC%80%E5%A7%8B%E4%BB%8E-RGB-%E8%BD%AC%E5%90%91-OKLCH-%E2%80%94%E2%80%94-%E9%A2%9C%E8%89%B2%E5%A4%84%E7%90%86%E7%9A%84%E6%96%B0%E9%98%B6%E6%AE%B5/"/>
    <published>2025-11-23T08:25:37.000Z</published>
    <summary>
      <![CDATA[<p>在做自己的项目时，我逐渐发现一个问题：<br>颜色越写越乱、越调越脏，很多看似简单的调色逻辑在实际效果上完全不符合预期。</p>
<p>比如：<br>    •	明明只是想调亮一点，但 RGB&#x2F;HSL 调完后颜色不是变灰就是变艳；<br>    •	做渐变时颜色中途莫名其妙发暗；<br>    •	图表配色在亮色主题和暗色主题下反差不一致；<br>    •	在宽色域（如 P3）设备上，颜色表现和预期差得更远。</p>
<p>这些都指向同一个底层原因：<br>RGB（甚至 HSL&#x2F;HSV）并不是一个符合人眼感知的色彩空间。</p>
<p>于是我开始把新的颜色计算逻辑逐步迁移到 OKLCH —— 一个感知均匀、现代化、已被浏览器原生支持的色彩空间。<br>这篇文章来分享我为什么做这个选择，也希望给你在项目色彩处理上的参考。</p>]]>
    </summary>
    <title>为什么我开始从 RGB 转向 OKLCH —— 颜色处理的新阶段</title>
    <updated>2025-11-23T08:38:34.209Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Photos" scheme="https://yuluod.github.io/categories/Photos/"/>
    <category term="exiftool" scheme="https://yuluod.github.io/tags/exiftool/"/>
    <content>
      <![CDATA[<p>在日常摄影中，我们常常会遇到这样一个问题：使用相机（如 Nikon Z30 等非 GPS 相机）拍摄的照片并没有记录位置信息。如果你希望在后期整理或分享时，让照片具备地理信息，就需要借助专业工具来补充或批量写入 GPS 数据。</p><p>本文将介绍如何使用强大的命令行工具 ExifTool 来解决这个问题，包括：<br>    1.为照片添加 GPS 信息；<br>    2.将一张照片的 GPS 信息复制到另一张或多张照片中；<br>    3.检查和验证修改结果。</p><span id="more"></span><p>什么是 ExifTool？</p><p>ExifTool 是一款非常强大的命令行工具，可以读取、写入和编辑图片、音视频等文件的元数据（Metadata）。它支持几乎所有的照片格式，包括 JPEG、PNG，以及各大相机品牌的 RAW 格式（NEF、CR2、ARW 等）。</p><p>官网地址：<a href="https://exiftool.org/">https://exiftool.org/</a></p><p>⸻</p><p>用 ExifTool 安全地把一张照片的 GPS 信息复制到其它照片（NEF&#x2F;RAW）——完整实操指南</p><p>参考：ExifTool 官方文档 <a href="https://exiftool.org/">https://exiftool.org/</a><br>本文将结论先行：ExifTool 支持直接往 RAW 文件（包括 NEF）写 GPS 信息，并提供默认的 _original 文件自动备份。</p><p>⸻</p><p>📝 概览（结论先行）<br>    •ExifTool 能够直接写入 RAW（NEF、CR2、ARW 等），GPS 信息可以和 EXIF、XMP 一起保存。<br>    •macOS 上推荐用 Homebrew 安装，也可从 exiftool.org 下载 .pkg。<br>    •操作建议：先备份 → 先验证 → 小规模试运行 → 再批量处理。</p><p>⸻</p><p>1）准备工作（安装 + 备份）</p><p>安装 ExifTool</p><p>推荐 Homebrew 安装：</p><p>brew install exiftool</p><p>或直接下载 Mac 安装包：<a href="https://exiftool.org/">https://exiftool.org/</a></p><p>验证安装：</p><p>exiftool -ver</p><p>备份</p><p>虽然 ExifTool 默认会保留 _original 文件（参考官方 Backups 说明），但强烈建议在大规模操作前完整复制目录。</p><p>⸻</p><p>2）读取并验证源文件 GPS 标签</p><p>在写入前先确认源文件 GPS 信息存在哪些 group：</p><h1 id="显示所有-GPS-相关字段，含组名"><a href="#显示所有-GPS-相关字段，含组名" class="headerlink" title="显示所有 GPS 相关字段，含组名"></a>显示所有 GPS 相关字段，含组名</h1><p>exiftool -a -G1 -s -gps* source_with_gps.nef</p><h1 id="只看-EXIF-GPS"><a href="#只看-EXIF-GPS" class="headerlink" title="只看 EXIF GPS"></a>只看 EXIF GPS</h1><p>exiftool -gps:all source_with_gps.nef</p><pre><code class="hljs">•-a：显示重复标签•-G1：显示标签组（EXIF、XMP 等）•-s：显示原始标签名</code></pre><p>（参见官方 Tag Groups 文档）</p><p>⸻</p><p>3）单张复制 GPS 信息</p><p>把源文件的 GPS 信息复制到目标文件：</p><p>exiftool -tagsFromFile source_with_gps.nef -gps:all target_without_gps.nef</p><pre><code class="hljs">•默认会生成 target_without_gps.nef_original（安全备份）。</code></pre><p>如果你已确认无误且不需要 _original：</p><p>exiftool -P -overwrite_original_in_place <br>  -tagsFromFile source_with_gps.nef -gps:all target_without_gps.nef</p><pre><code class="hljs">•-P：保留文件时间戳•-overwrite_original_in_place：覆盖原文件，并尽量保持文件系统属性（官方 In-place Editing）</code></pre><p>⸻</p><p>4）批量操作：一张模板 → 整个目录</p><p>假设 source.jpg 含 GPS，要把其 GPS 写入当前目录下所有 NEF：</p><p>cd &#x2F;path&#x2F;to&#x2F;folder<br>exiftool -tagsFromFile ..&#x2F;source.jpg -gps:all -ext NEF -r .</p><p>解释：<br>    •-ext NEF：只处理 NEF 文件<br>    •-r .：递归处理子目录<br>    •避免 *.*，否则可能匹配 _original 或 .DS_Store 文件（官方 FAQ 有提醒）</p><p>覆盖写入（危险操作，需先备份）：</p><p>exiftool -P -overwrite_original_in_place <br>  -tagsFromFile ..&#x2F;source.jpg -gps:all -ext NEF -r .</p><p>⸻</p><p>5）从 XMP Sidecar 导入 GPS</p><p>如果 NX Studio 把 GPS 写在 .xmp 文件：</p><p>exiftool -tagsFromFile image.nef.xmp -gps:all image.nef</p><p>ExifTool 会把 .xmp 里的 GPS 信息同步到 NEF 文件（参考 XMP Tag Handling）。</p><p>⸻</p><p>6）按 GPX 轨迹打点</p><p>若有 GPX 轨迹文件，可根据照片时间自动匹配 GPS：</p><p>exiftool -geotag track.gpx *.NEF</p><p>支持时区偏移、插值（详见官方 Geotagging 文档）。</p><p>⸻</p><p>7）手动写入经纬度</p><p>如果你手动知道坐标：</p><p>exiftool -GPSLatitude&#x3D;39.9042 -GPSLatitudeRef&#x3D;N <br>  -GPSLongitude&#x3D;116.4074 -GPSLongitudeRef&#x3D;E photo.nef</p><p>使用 -n 可避免格式化，直接写入数值。</p><p>⸻</p><p>8）验证与回滚</p><p>验证写入结果</p><p>exiftool -a -G1 -s -gps* target.nef</p><p>确认 GPSLatitude、GPSLongitude、GPSDateStamp 等标签已写入。</p><p>回滚方法<br>    •如果有 _original 文件：手动还原或使用：</p><p>exiftool -restore_original target.nef</p><pre><code class="hljs">•若已用 -overwrite_original_in_place：只能依赖事前备份。</code></pre><p>⸻</p><p>9）常见坑 &amp; 最佳实践</p><p>✅ 先备份，再操作<br>✅ 先测试 2–3 张，再批量运行<br>✅ 避免 *.*，用 -ext 精确指定文件类型<br>✅ 注意不同软件读取的 group（EXIF vs XMP）<br>✅ 保留 _original 是自动安全网</p><p>⸻</p><p>10）批量脚本示例（macOS &#x2F; bash）</p><p>保存为 copy_gps_from_template.sh，然后：</p><p>chmod +x copy_gps_from_template.sh<br>.&#x2F;copy_gps_from_template.sh source_with_gps.jpg &#x2F;path&#x2F;to&#x2F;target_dir</p><p>脚本内容：</p><p>#!&#x2F;usr&#x2F;bin&#x2F;env bash<br>set -euo pipefail</p><p>SOURCE&#x3D;”$1”         # 模板文件（有 GPS 的图片）<br>TARGET_DIR&#x3D;”$2”     # 目标目录<br>EXT&#x3D;”NEF”           # 要处理的扩展名</p><p>if [ -z “$SOURCE” ] || [ -z “$TARGET_DIR” ]; then<br>  echo “Usage: $0 <source-with-gps> <target-dir>“<br>  exit 1<br>fi</p><p>echo “Dry-run: files that WOULD be processed:”<br>find “$TARGET_DIR” -type f -iname “*.${EXT}” -print | sed -n ‘1,50p’<br>echo “—-“<br>echo “To actually run, edit script and uncomment exiftool command.”</p><h1 id="实际运行（保留-original）"><a href="#实际运行（保留-original）" class="headerlink" title="实际运行（保留 _original）"></a>实际运行（保留 _original）</h1><h1 id="exiftool-tagsFromFile-“-SOURCE”-gps-all-ext-“-EXT”-r-“-TARGET-DIR”"><a href="#exiftool-tagsFromFile-“-SOURCE”-gps-all-ext-“-EXT”-r-“-TARGET-DIR”" class="headerlink" title="exiftool -tagsFromFile “$SOURCE” -gps:all -ext “$EXT” -r “$TARGET_DIR”"></a>exiftool -tagsFromFile “$SOURCE” -gps:all -ext “$EXT” -r “$TARGET_DIR”</h1><h1 id="覆盖原文件（危险）"><a href="#覆盖原文件（危险）" class="headerlink" title="覆盖原文件（危险）"></a>覆盖原文件（危险）</h1><h1 id="OVERWRITE-FLAGS-”-P-overwrite-original-in-place”"><a href="#OVERWRITE-FLAGS-”-P-overwrite-original-in-place”" class="headerlink" title="OVERWRITE_FLAGS&#x3D;”-P -overwrite_original_in_place”"></a>OVERWRITE_FLAGS&#x3D;”-P -overwrite_original_in_place”</h1><h1 id="exiftool-OVERWRITE-FLAGS-tagsFromFile-“-SOURCE”-gps-all-ext-“-EXT”-r-“-TARGET-DIR”"><a href="#exiftool-OVERWRITE-FLAGS-tagsFromFile-“-SOURCE”-gps-all-ext-“-EXT”-r-“-TARGET-DIR”" class="headerlink" title="exiftool $OVERWRITE_FLAGS -tagsFromFile “$SOURCE” -gps:all -ext “$EXT” -r “$TARGET_DIR”"></a>exiftool $OVERWRITE_FLAGS -tagsFromFile “$SOURCE” -gps:all -ext “$EXT” -r “$TARGET_DIR”</h1><p>echo “Done. Verify with: exiftool -a -G1 -s -gps* <file>“</p><p>⸻</p><p>📌 总结<br>    •ExifTool 是处理照片 GPS 信息的标准工具。<br>    •既可以从一张“模板”照片复制 GPS，也可以从 XMP sidecar 或 GPX 轨迹导入。<br>    •默认 _original 文件机制提供了安全保障，建议初期不要关闭。<br>    •批量处理时，要谨慎，推荐小规模试运行。</p><p>⸻</p><p>要不要我帮你再写一份 针对“先小规模测试 → 大规模批处理”的实操清单（比如一个三步 checklist，你只要按顺序执行，就能避免 90% 的坑）？</p>]]>
    </content>
    <id>https://yuluod.github.io/2025/09/30/exiftool-%E4%BD%BF%E7%94%A8/</id>
    <link href="https://yuluod.github.io/2025/09/30/exiftool-%E4%BD%BF%E7%94%A8/"/>
    <published>2025-09-30T05:59:40.000Z</published>
    <summary>
      <![CDATA[<p>在日常摄影中，我们常常会遇到这样一个问题：使用相机（如 Nikon Z30 等非 GPS 相机）拍摄的照片并没有记录位置信息。如果你希望在后期整理或分享时，让照片具备地理信息，就需要借助专业工具来补充或批量写入 GPS 数据。</p>
<p>本文将介绍如何使用强大的命令行工具 ExifTool 来解决这个问题，包括：<br>    1.	为照片添加 GPS 信息；<br>    2.	将一张照片的 GPS 信息复制到另一张或多张照片中；<br>    3.	检查和验证修改结果。</p>]]>
    </summary>
    <title>使用 ExifTool 为照片批量添加和复制 GPS 信息</title>
    <updated>2025-09-30T14:49:57.051Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <content>
      <![CDATA[<p>Python 生态中有许多包管理工具，如 <code>pip</code>、<code>poetry</code> 和 <code>conda</code>，而 <code>uv</code> 作为一个新兴工具，正迅速受到关注。<code>uv</code> 由 Astral 开发，基于 Rust 编写，宣称是 Python 包管理的未来。它不仅速度极快（据称比传统工具快 10-100 倍），还集成了项目管理、虚拟环境管理和 Python 版本管理的功能，旨在成为一个全能解决方案。</p><p>截至 2025 年 3 月 29 日，<code>uv</code> 的官方文档和社区反馈显示，其普及度正在提升，尤其在需要高效依赖管理的场景中表现出色。然而，作为一个较新的工具，其长期稳定性仍需更多用户验证。</p><span id="more"></span><h3 id="安装步骤"><a href="#安装步骤" class="headerlink" title="安装步骤"></a>安装步骤</h3><p><code>uv</code> 提供了多种安装方法，适用于不同操作系统。以下是最新且推荐的安装步骤：</p><ul><li><p><strong>使用安装脚本：</strong></p><ul><li><strong>Linux 系统：</strong><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -LsSf https://astral.sh/uv/install.sh | sh<br></code></pre></td></tr></table></figure></li><li><strong>macOS 系统：</strong><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">brew install uv<br></code></pre></td></tr></table></figure></li><li><strong>Windows（PowerShell）：</strong><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs powershell">powershell <span class="hljs-literal">-ExecutionPolicy</span> Bypass <span class="hljs-literal">-c</span> <span class="hljs-string">&quot;irm https://astral.sh/uv/install.ps1 | iex&quot;</span><br></code></pre></td></tr></table></figure><em>注意</em>：Windows 用户可能需调整 PowerShell 执行策略，确保脚本顺利运行。</li><li>这些命令会自动下载并安装 <code>uv</code>，无需额外配置。</li></ul></li><li><p><strong>使用 pip 安装：</strong></p><ul><li>运行：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pip install uv<br></code></pre></td></tr></table></figure></li><li>或使用 <code>pipx</code>：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pipx install uv<br></code></pre></td></tr></table></figure></li><li>更多安装选项可参考 <a href="https://docs.astral.sh/uv/get-started/installation/">uv 官方安装文档</a>。</li></ul></li></ul><p><strong>验证安装：</strong><br>安装完成后，运行以下命令检查版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv --version<br></code></pre></td></tr></table></figure><p>输出类似 <code>uv 0.1.0</code>，表示安装成功。</p><hr><h3 id="项目初始化与依赖管理"><a href="#项目初始化与依赖管理" class="headerlink" title="项目初始化与依赖管理"></a>项目初始化与依赖管理</h3><p><code>uv</code> 的核心功能之一是简化项目初始化和依赖管理。以下是具体用法：</p><ul><li><p><strong>初始化项目：</strong></p><ul><li>在项目目录中运行：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv project init<br></code></pre></td></tr></table></figure></li><li>此命令会生成 <code>uv.toml</code> 文件，用于存储项目配置。</li></ul></li><li><p><strong>添加依赖：</strong></p><ul><li>添加包（如 <code>requests</code>）：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv add requests<br></code></pre></td></tr></table></figure></li><li><code>uv</code> 会快速解析并安装依赖到项目虚拟环境中。</li></ul></li><li><p><strong>更新和移除依赖：</strong></p><ul><li>更新所有依赖：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">sync</span> --upgrade<br></code></pre></td></tr></table></figure></li><li>更新特定包：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">sync</span> --upgrade-package &lt;package_name&gt; <br></code></pre></td></tr></table></figure></li><li>移除包：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv remove &lt;package_name&gt;<br></code></pre></td></tr></table></figure></li></ul></li><li><p><strong>锁文件（lockfile）：</strong></p><ul><li>运行 <code>uv lock</code> 生成锁文件，确保依赖版本一致，适合团队协作。</li></ul></li></ul><hr><h3 id="虚拟环境管理"><a href="#虚拟环境管理" class="headerlink" title="虚拟环境管理"></a>虚拟环境管理</h3><p><code>uv</code> 提供便捷的虚拟环境管理功能：</p><ul><li><p><strong>创建和激活：</strong></p><ul><li><code>uv</code> 自动为项目创建虚拟环境，激活方式：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv activate<br></code></pre></td></tr></table></figure></li><li>激活后，命令行提示符会显示虚拟环境名称。</li></ul></li><li><p><strong>退出虚拟环境：</strong></p><ul><li>输入：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv deactivate<br></code></pre></td></tr></table></figure></li></ul></li><li><p><strong>列出虚拟环境：</strong></p><ul><li>查看所有管理的虚拟环境：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv <span class="hljs-built_in">env</span> list<br></code></pre></td></tr></table></figure></li></ul></li><li><p><strong>多版本支持：</strong></p><ul><li>通过 <code>uv python install &lt;version&gt;</code> 安装特定 Python 版本（如 <code>3.10</code>），并自动检测系统已有 Python 安装。</li></ul></li></ul><hr><h3 id="运行脚本和工具"><a href="#运行脚本和工具" class="headerlink" title="运行脚本和工具"></a>运行脚本和工具</h3><p><code>uv</code> 支持运行脚本和安装工具，类似 <code>pipx</code>：</p><ul><li><p><strong>运行脚本：</strong></p><ul><li>执行 Python 脚本：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv run &lt;script_path&gt;<br></code></pre></td></tr></table></figure>示例：<code>uv run myscript.py</code>。</li></ul></li><li><p><strong>安装工具：</strong></p><ul><li>安装工具包（如 <code>black</code>）：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv tool install black<br></code></pre></td></tr></table></figure></li><li>运行工具：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv tool run black myfile.py<br></code></pre></td></tr></table></figure>或使用别名 <code>uvx</code>：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uvx black myfile.py<br></code></pre></td></tr></table></figure></li></ul></li><li><p><strong>临时虚拟环境：</strong></p><ul><li><code>uv tool run</code> 会创建临时环境运行工具，用后即删，保持项目环境干净。</li></ul></li></ul><hr><h3 id="Python-版本管理"><a href="#Python-版本管理" class="headerlink" title="Python 版本管理"></a>Python 版本管理</h3><p><code>uv</code> 内置 Python 版本管理，适合多版本开发：</p><ul><li><p><strong>安装版本：</strong></p><ul><li>安装指定版本：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv python install 3.10<br></code></pre></td></tr></table></figure></li><li>可同时安装多个版本：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv python install 3.9 3.10 3.11<br></code></pre></td></tr></table></figure></li></ul></li><li><p><strong>列出和使用：</strong></p><ul><li>查看已安装版本：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv python list<br></code></pre></td></tr></table></figure></li><li>在 <code>uv.toml</code> 中固定版本：<figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs toml"><span class="hljs-section">[tool.uv]</span><br><span class="hljs-attr">python</span> = <span class="hljs-string">&quot;3.10&quot;</span><br></code></pre></td></tr></table></figure></li><li>或运行时指定：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">uv --python 3.10 run myscript.py<br></code></pre></td></tr></table></figure></li></ul></li></ul><hr><h3 id="高级功能"><a href="#高级功能" class="headerlink" title="高级功能"></a>高级功能</h3><p><code>uv</code> 提供了一些高级特性：</p><ul><li><p><strong>锁文件和工作区：</strong></p><ul><li><code>uv lock</code> 生成锁文件。</li><li>支持工作区管理，在父目录创建 <code>uv.workspace.toml</code>。</li></ul></li><li><p><strong>兼容 pip：</strong></p><ul><li>支持 <code>uv pip install &lt;package_name&gt;</code>，性能远超传统 <code>pip</code>。</li></ul></li></ul><hr><h3 id="疑难解答与技巧"><a href="#疑难解答与技巧" class="headerlink" title="疑难解答与技巧"></a>疑难解答与技巧</h3><p>以下是常见问题解决方法：</p><ul><li><strong>更新 uv：</strong><ul><li>运行 <code>uv self update</code> 获取最新版本。</li></ul></li><li><strong>清理环境：</strong><ul><li>用 <code>uv clean</code> 重置项目环境。</li></ul></li><li><strong>获取帮助：</strong><ul><li>参考 <a href="https://docs.astral.sh/uv/">uv 官方文档</a> 或社区讨论。</li></ul></li></ul><hr><h3 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h3><p><code>uv</code> 是一个高效、多功能的 Python 包管理工具，适合追求速度和集成体验的开发者。虽然它可能改变传统工作流，但作为新工具，建议先在非关键项目中试用。希望本教程能帮助您快速掌握 <code>uv</code>，提升开发效率！</p>]]>
    </content>
    <id>https://yuluod.github.io/2025/03/30/Python-%E6%96%B0%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7%EF%BC%9AUV/</id>
    <link href="https://yuluod.github.io/2025/03/30/Python-%E6%96%B0%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7%EF%BC%9AUV/"/>
    <published>2025-03-30T12:28:43.000Z</published>
    <summary>
      <![CDATA[<p>Python 生态中有许多包管理工具，如 <code>pip</code>、<code>poetry</code> 和 <code>conda</code>，而 <code>uv</code> 作为一个新兴工具，正迅速受到关注。<code>uv</code> 由 Astral 开发，基于 Rust 编写，宣称是 Python 包管理的未来。它不仅速度极快（据称比传统工具快 10-100 倍），还集成了项目管理、虚拟环境管理和 Python 版本管理的功能，旨在成为一个全能解决方案。</p>
<p>截至 2025 年 3 月 29 日，<code>uv</code> 的官方文档和社区反馈显示，其普及度正在提升，尤其在需要高效依赖管理的场景中表现出色。然而，作为一个较新的工具，其长期稳定性仍需更多用户验证。</p>]]>
    </summary>
    <title>Python 新包管理工具：UV</title>
    <updated>2025-09-22T03:42:25.045Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="DEV" scheme="https://yuluod.github.io/categories/DEV/"/>
    <category term="docker" scheme="https://yuluod.github.io/tags/docker/"/>
    <content>
      <![CDATA[<p>在国内使用 Docker 时，拉取官方镜像可能会遇到速度缓慢或连接超时的问题。这主要是由于网络环境和访问 Docker 官方镜像仓库（Docker Hub）时的带宽限制所致。为了解决这一问题，配置国内的 Docker 镜像加速器（registry mirrors）是一种有效的解决方案。</p><p>Docker 镜像加速器是由国内服务提供商搭建的镜像仓库，通过同步 Docker Hub 的内容，提供更快的镜像拉取速度。这些加速器能够有效减少镜像下载时间，提高开发和部署效率。</p><span id="more"></span><p>常用的国内 Docker 镜像加速器</p><p>以下是一些常用的国内 Docker 镜像加速器：</p><blockquote><p>•<a href="https://cr.laoyou.ip-ddns.com/">https://cr.laoyou.ip-ddns.com</a><br>    •<a href="https://docker.1panel.live/">https://docker.1panel.live</a><br>    •<a href="https://image.cloudlayer.icu/">https://image.cloudlayer.icu</a><br>    •<a href="https://hub.fast360.xyz/">https://hub.fast360.xyz</a><br>    •<a href="https://docker-0.unsee.tech/">https://docker-0.unsee.tech</a><br>    •<a href="https://docker.1panelproxy.com/">https://docker.1panelproxy.com</a><br>    •<a href="https://docker.tbedu.top/">https://docker.tbedu.top</a><br>    •<a href="https://dockerpull.cn/">https://dockerpull.cn</a><br>    •<a href="https://docker.m.daocloud.io/">https://docker.m.daocloud.io</a><br>    •<a href="https://hub.rat.dev/">https://hub.rat.dev</a><br>    •<a href="https://docker.kejilion.pro/">https://docker.kejilion.pro</a><br>    •<a href="https://docker.hlmirror.com/">https://docker.hlmirror.com</a><br>    •<a href="https://docker.imgdb.de/">https://docker.imgdb.de</a><br>    •<a href="https://docker.melikeme.cn/">https://docker.melikeme.cn</a><br>    •<a href="https://ccr.ccs.tencentyun.com/">https://ccr.ccs.tencentyun.com</a><br>    •<a href="https://pull.loridocker.com/">https://pull.loridocker.com</a></p></blockquote><p>要配置 Docker 使用上述镜像加速器，可以按照以下步骤进行：<br>    1. 创建或编辑 Docker 配置文件：在终端中执行以下命令：</p><pre><code class="hljs"> sudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json &lt;&lt;-&#39;EOF&#39;&#123;    &quot;registry-mirrors&quot;: [        &quot;https://cr.laoyou.ip-ddns.com&quot;,        &quot;https://docker.1panel.live&quot;,        &quot;https://image.cloudlayer.icu&quot;,        &quot;https://hub.fast360.xyz&quot;,        &quot;https://docker-0.unsee.tech&quot;,        &quot;https://docker.1panelproxy.com&quot;,        &quot;https://docker.tbedu.top&quot;,        &quot;https://dockerpull.cn&quot;,        &quot;https://docker.m.daocloud.io&quot;,        &quot;https://hub.rat.dev&quot;,        &quot;https://docker.kejilion.pro&quot;,        &quot;https://docker.hlmirror.com&quot;,        &quot;https://docker.imgdb.de&quot;,        &quot;https://docker.melikeme.cn&quot;,        &quot;https://ccr.ccs.tencentyun.com&quot;,        &quot;https://pull.loridocker.com&quot;    ]&#125;EOF</code></pre><ol><li>重启 Docker 服务：使配置生效： <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash">    sudo systemctl daemon-reload<br>    sudo systemctl restart docker<br>    <br>1. 验证配置：使用以下命令检查配置是否生效：<br>    ```bash<br>    docker info<br></code></pre></td></tr></table></figure> 在输出中，应看到类似以下内容： <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">Registry Mirrors:<br>https://cr.laoyou.ip-ddns.com/<br>https://docker.1panel.live/<br>...<br></code></pre></td></tr></table></figure></li></ol><p>这表明 Docker 已成功配置了国内镜像加速器。<br>注意事项</p><blockquote><pre><code class="hljs">•镜像加速器的可用性：由于网络环境和服务提供商的维护情况，镜像加速器的可用性可能会发生变化。建议定期检查并更新配置，以确保使用的镜像加速器稳定可靠。•安全性考虑：使用镜像加速器时，应选择信誉良好的服务提供商，以确保镜像的完整性和安全性。</code></pre></blockquote>]]>
    </content>
    <id>https://yuluod.github.io/2025/03/18/Docker-%E9%95%9C%E5%83%8F%E6%8B%89%E5%8F%96%E5%A4%B1%E8%B4%A5/</id>
    <link href="https://yuluod.github.io/2025/03/18/Docker-%E9%95%9C%E5%83%8F%E6%8B%89%E5%8F%96%E5%A4%B1%E8%B4%A5/"/>
    <published>2025-03-18T06:00:51.000Z</published>
    <summary>
      <![CDATA[<p>在国内使用 Docker 时，拉取官方镜像可能会遇到速度缓慢或连接超时的问题。这主要是由于网络环境和访问 Docker 官方镜像仓库（Docker Hub）时的带宽限制所致。为了解决这一问题，配置国内的 Docker 镜像加速器（registry mirrors）是一种有效的解决方案。</p>
<p>Docker 镜像加速器是由国内服务提供商搭建的镜像仓库，通过同步 Docker Hub 的内容，提供更快的镜像拉取速度。这些加速器能够有效减少镜像下载时间，提高开发和部署效率。</p>]]>
    </summary>
    <title>Docker 镜像拉取失败</title>
    <updated>2025-03-18T06:10:28.969Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <category term="MarkItDown" scheme="https://yuluod.github.io/tags/MarkItDown/"/>
    <content>
      <![CDATA[<p><strong>MarkItDown</strong> 是微软开源的一款多功能文档转换工具，旨在将多种文件格式（如 PDF、Office 文档、图像、音频等）高效转换为 <strong>Markdown</strong> 格式，适用于文档索引、文本分析、内容管理等场景。以下是其核心特性与优势的详细解析：</p><span id="more"></span><hr><h3 id="1-核心功能与支持格式"><a href="#1-核心功能与支持格式" class="headerlink" title="1. 核心功能与支持格式"></a>1. <strong>核心功能与支持格式</strong></h3><p>MarkItDown 支持广泛的文件类型转换，覆盖了办公、多媒体及常见文本格式：</p><ul><li><strong>办公文档</strong>：PDF（提取结构化内容）、Word（保留基础格式和注释）、Excel（表格转Markdown表格）、PowerPoint（提取幻灯片文本和图表）。</li><li><strong>图像与音频</strong>：提取 EXIF 元数据、OCR 文字识别（图片）、语音转文本（音频）。</li><li><strong>其他格式</strong>：HTML（优化维基百科等复杂页面）、CSV&#x2F;JSON&#x2F;XML（结构化数据转Markdown）、ZIP 压缩包（批量处理内部文件）。</li><li><strong>AI 集成</strong>：支持调用 OpenAI 等大语言模型（如 GPT-4），为图片生成描述性文本或增强语义分析。</li></ul><hr><h3 id="2-技术实现与依赖库"><a href="#2-技术实现与依赖库" class="headerlink" title="2. 技术实现与依赖库"></a>2. <strong>技术实现与依赖库</strong></h3><p>MarkItDown 基于模块化设计，整合了多个高效的开源库以实现复杂文件处理：</p><ul><li><strong>文档解析</strong>：  <ul><li>PDF 使用 <code>pdfminer.six</code> 提取文本，Word 依赖 <code>mammoth</code>，Excel 基于 <code>openpyxl</code>，PPT 使用 <code>python-pptx</code>。</li><li>HTML 通过 <code>beautifulsoup4</code> 和 <code>markdownify</code> 清理并转换格式。</li></ul></li><li><strong>多媒体处理</strong>：  <ul><li>音频转文本通过 <code>SpeechRecognition</code> 库实现，OCR 使用 <code>EasyOCR</code>。</li><li>图像描述需配置 ExifTool（元数据提取）和 LLM（如 OpenAI 的 GPT-4）。</li></ul></li><li><strong>其他工具</strong>：  <ul><li>字符编码处理依赖 <code>charset-normalizer</code>，文件类型检测使用 <code>puremagic</code>。</li></ul></li></ul><hr><h3 id="3-应用场景与优劣"><a href="#3-应用场景与优劣" class="headerlink" title="3. 应用场景与优劣"></a>3. <strong>应用场景与优劣</strong></h3><p>MarkItDown 的灵活性和高效性使其在多个领域发挥作用：</p><ul><li><strong>自动化文档管理</strong>：批量转换混合格式文件为统一 Markdown，便于版本控制或知识库构建。</li><li><strong>AI 数据预处理</strong>：将非结构化数据（如 PDF、音频）转换为可分析的文本，提升大语言模型训练效率。</li><li><strong>无障碍支持</strong>：转录音频内容为文本，或从图像中提取文字，助力无障碍工作流。</li><li><strong>开发与协作</strong>：开发者可快速提取文档内容进行索引或分析，团队协作中 Markdown 格式更易维护。</li><li><strong>劣势</strong>：不支持doc和xls等前一代的Office格式，对于PDF中的图片也没有办法，甚至PDF过大也不行。</li></ul><hr><h3 id="4-开源与社区支持"><a href="#4-开源与社区支持" class="headerlink" title="4. 开源与社区支持"></a>4. <strong>开源与社区支持</strong></h3><ul><li><strong>开源协议</strong>：遵循 MIT 许可证，允许自由使用、修改和分发，需保留原始版权声明。</li><li><strong>社区贡献</strong>：微软鼓励开发者参与插件开发（支持第三方扩展）及问题修复，GitHub 提供详细贡献指南和测试框架。</li><li><strong>持续优化</strong>：用户反馈的已知问题（如图片转换失败、PDF 编码错误）正在逐步解决。</li></ul><hr>]]>
    </content>
    <id>https://yuluod.github.io/2025/02/11/MarkItDown-%E7%9C%9F%E7%9A%84%E5%A5%BD%E4%B9%88%EF%BC%9F/</id>
    <link href="https://yuluod.github.io/2025/02/11/MarkItDown-%E7%9C%9F%E7%9A%84%E5%A5%BD%E4%B9%88%EF%BC%9F/"/>
    <published>2025-02-11T06:05:22.000Z</published>
    <summary>
      <![CDATA[<p><strong>MarkItDown</strong> 是微软开源的一款多功能文档转换工具，旨在将多种文件格式（如 PDF、Office 文档、图像、音频等）高效转换为 <strong>Markdown</strong> 格式，适用于文档索引、文本分析、内容管理等场景。以下是其核心特性与优势的详细解析：</p>]]>
    </summary>
    <title>MarkItDown 真的好么？</title>
    <updated>2025-02-11T07:13:28.869Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Life" scheme="https://yuluod.github.io/categories/Life/"/>
    <category term="生活" scheme="https://yuluod.github.io/tags/%E7%94%9F%E6%B4%BB/"/>
    <content>
      <![CDATA[<p>一年一年又一年<br>转身发现自己过了这么多年<br>依旧一事无成</p><p>倔强要来的娃<br>让我多了一个身份<br>家庭多了一个成员<br>软萌可爱<br>却又有时脾气恶劣<br>天使魔鬼结合体</p><p>从看到娃的那时起<br>软肋铠甲并存<br>想了太多太多<br>她嫌我想的太远<br>我却不知道能否保证娃的健康快乐</p><p>因为娃<br>我跟她疲惫不堪<br>得靠着娃的笑容充电</p><p>值么？<br>不知道<br>但<br>也没得选<br>对么？</p><p>2024年的如今一团糟<br>2025年的以后还会更糟么？</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/12/31/2024%E5%B9%B4%E7%9A%84%E5%A6%82%E4%BB%8A/</id>
    <link href="https://yuluod.github.io/2024/12/31/2024%E5%B9%B4%E7%9A%84%E5%A6%82%E4%BB%8A/"/>
    <published>2024-12-31T15:43:37.000Z</published>
    <summary>
      <![CDATA[<p>一年一年又一年<br>转身发现自己过了这么多年<br>依旧一事无成</p>
<p>倔强要来的娃<br>让我多了一个身份<br>家庭多了一个成员<br>软萌可爱<br>却又有时脾气恶劣<br>天使魔鬼结合体</p>
<p>从看到娃的那时起<br>软肋铠甲并存<br>想了太多太]]>
    </summary>
    <title>2024年的如今</title>
    <updated>2024-12-31T16:01:49.487Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <category term="JSON" scheme="https://yuluod.github.io/tags/JSON/"/>
    <content>
      <![CDATA[<p>在 Python 中，JSON 解析和序列化是非常常见的任务。虽然标准库提供了 <code>json</code> 模块，但当数据量大或者性能要求高时，它可能无法满足需求。这时候可以选择第三方库，比如 <strong>ORJSON</strong>。</p><p>ORJSON 是一个超快速且功能丰富的 JSON 解析和序列化库，它利用 Rust 实现，性能显著优于 Python 标准库和其他 JSON 库（如 <code>ujson</code> 和 <code>rapidjson</code>）。</p><span id="more"></span><h3 id="ORJSON-的优势"><a href="#ORJSON-的优势" class="headerlink" title="ORJSON 的优势"></a>ORJSON 的优势</h3><ol><li><strong>高性能</strong>：在序列化和反序列化速度上，ORJSON 是当前最快的 Python JSON 库之一。</li><li><strong>支持复杂数据类型</strong>：原生支持 <code>datetime</code>、<code>numpy</code> 数组等复杂数据类型的序列化，而无需额外处理。</li><li><strong>严格的标准化支持</strong>：ORJSON 完全符合 JSON 标准，并支持 UTF-8 编码。</li><li><strong>易用性</strong>：API 设计简单直观，几乎可以直接替换标准库 <code>json</code> 模块的功能。</li></ol><hr><h3 id="安装-ORJSON"><a href="#安装-ORJSON" class="headerlink" title="安装 ORJSON"></a>安装 ORJSON</h3><p>在项目中安装 ORJSON 非常简单，只需运行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">pip install orjson<br></code></pre></td></tr></table></figure><hr><h3 id="ORJSON-基本用法"><a href="#ORJSON-基本用法" class="headerlink" title="ORJSON 基本用法"></a>ORJSON 基本用法</h3><h4 id="1-JSON-序列化"><a href="#1-JSON-序列化" class="headerlink" title="1. JSON 序列化"></a>1. <strong>JSON 序列化</strong></h4><p>ORJSON 的序列化使用 <code>orjson.dumps</code> 函数，返回的结果是字节类型（<code>bytes</code>），可以轻松用于文件写入或网络传输。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> orjson<br><br><span class="hljs-comment"># 示例数据</span><br>data = &#123;<span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;Alice&quot;</span>, <span class="hljs-string">&quot;age&quot;</span>: <span class="hljs-number">25</span>, <span class="hljs-string">&quot;is_student&quot;</span>: <span class="hljs-literal">False</span>&#125;<br><br><span class="hljs-comment"># 序列化为 JSON 字节串</span><br>json_bytes = orjson.dumps(data)<br><span class="hljs-built_in">print</span>(json_bytes.decode(<span class="hljs-string">&quot;utf-8&quot;</span>))  <span class="hljs-comment"># 转换为字符串输出</span><br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;Alice&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;age&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">25</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;is_student&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><h4 id="2-JSON-反序列化"><a href="#2-JSON-反序列化" class="headerlink" title="2. JSON 反序列化"></a>2. <strong>JSON 反序列化</strong></h4><p>ORJSON 的反序列化使用 <code>orjson.loads</code>，可以将 JSON 字符串或字节串快速解析为 Python 对象。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python">json_data = <span class="hljs-string">b&#x27;&#123;&quot;name&quot;:&quot;Alice&quot;,&quot;age&quot;:25,&quot;is_student&quot;:false&#125;&#x27;</span><br>parsed_data = orjson.loads(json_data)<br><span class="hljs-built_in">print</span>(parsed_data)<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">&#123;<span class="hljs-string">&#x27;name&#x27;</span>: <span class="hljs-string">&#x27;Alice&#x27;</span>, <span class="hljs-string">&#x27;age&#x27;</span>: <span class="hljs-number">25</span>, <span class="hljs-string">&#x27;is_student&#x27;</span>: <span class="hljs-literal">False</span>&#125;<br></code></pre></td></tr></table></figure><h4 id="3-处理日期和时间"><a href="#3-处理日期和时间" class="headerlink" title="3. 处理日期和时间"></a>3. <strong>处理日期和时间</strong></h4><p>ORJSON 支持直接序列化 <code>datetime</code> 对象，无需额外格式化。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> orjson<br><span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime<br><br>data = &#123;<span class="hljs-string">&quot;timestamp&quot;</span>: datetime.utcnow()&#125;<br><br><span class="hljs-comment"># 序列化</span><br>json_bytes = orjson.dumps(data, option=orjson.OPT_NAIVE_UTC)<br><span class="hljs-built_in">print</span>(json_bytes.decode(<span class="hljs-string">&quot;utf-8&quot;</span>))<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;timestamp&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;2024-12-18T10:15:30.000000Z&quot;</span><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><hr><h3 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a>性能对比</h3><p>以下是 ORJSON 与其他 JSON 库的简单性能对比（序列化 100 万条数据）：</p><table><thead><tr><th>库</th><th>序列化时间（秒）</th><th>反序列化时间（秒）</th></tr></thead><tbody><tr><td><code>orjson</code></td><td><strong>0.03</strong></td><td><strong>0.04</strong></td></tr><tr><td><code>ujson</code></td><td>0.09</td><td>0.07</td></tr><tr><td><code>json</code></td><td>0.13</td><td>0.11</td></tr></tbody></table><p>从表中可以看出，ORJSON 的性能显著优于其他库，非常适合大数据量的 JSON 操作。</p><hr><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>ORJSON 是一款性能卓越的 JSON 库，它以极快的速度、简洁的 API 和出色的功能支持，成为处理 JSON 数据的理想选择。</p><hr><p>可以在 <a href="https://github.com/ijl/orjson">ORJSON 的官方文档</a> 中了解更多细节。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/12/18/%E4%B8%80%E4%B8%AA%E4%BD%BF%E7%94%A8%E7%9A%84Python%E4%B8%8B%E7%9A%84json%E5%BA%93%EF%BC%9Aorjson/</id>
    <link href="https://yuluod.github.io/2024/12/18/%E4%B8%80%E4%B8%AA%E4%BD%BF%E7%94%A8%E7%9A%84Python%E4%B8%8B%E7%9A%84json%E5%BA%93%EF%BC%9Aorjson/"/>
    <published>2024-12-18T07:50:42.000Z</published>
    <summary>
      <![CDATA[<p>在 Python 中，JSON 解析和序列化是非常常见的任务。虽然标准库提供了 <code>json</code> 模块，但当数据量大或者性能要求高时，它可能无法满足需求。这时候可以选择第三方库，比如 <strong>ORJSON</strong>。</p>
<p>ORJSON 是一个超快速且功能丰富的 JSON 解析和序列化库，它利用 Rust 实现，性能显著优于 Python 标准库和其他 JSON 库（如 <code>ujson</code> 和 <code>rapidjson</code>）。</p>]]>
    </summary>
    <title>一个使用的Python下的JSON库：ORJSON</title>
    <updated>2024-12-18T07:59:30.344Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <content>
      <![CDATA[<p>简而言之 time 模块中的 time（）函数用于获取当前时间，perf_counter（）函数用于获取当前时间的精确计时器值，通常用于测量程序的执行时间。</p><span id="more"></span><p>主要的区别和应用：</p><h2 id="time-perf-counter"><a href="#time-perf-counter" class="headerlink" title="time.perf_counter()"></a>time.perf_counter()</h2><p>用途: </p><ul><li>time.perf_counter() 返回一个高精度的时间计数器值，用于测量短时间间隔的准确时间。在某些系统上，它可能会使用高精度的时钟源来提供精确到纳秒级的时间戳。<br>特点:</li><li>高分辨率：通常适用于性能测量、算法测试，以及任何需要高精度时间测量的场景。</li><li>不保证从某个固定的时间点（如UNIX纪元时间1970-01-01）开始，只能用于测量时间的流逝。<br>在一些系统中，它可能会受到操作系统的时钟调整影响较小，保持高精度。</li></ul><h2 id="time-time"><a href="#time-time" class="headerlink" title="time.time()"></a>time.time()</h2><p>用途: </p><ul><li>time.time() 返回当前的UNIX时间戳，从1970年1月1日（UTC）开始的秒数。它主要用于获取当前的绝对时间。<br>特点:</li><li>时间戳：用于获取当前时间的绝对值，适合表示具体的日期和时间。</li><li>精度通常在毫秒级：虽然可以提供小数部分，但其精度通常不及 time.perf_counter()。</li><li>可能会受到系统时间调整的影响，例如手动更改系统时间或自动同步时间。</li></ul><p>使用建议</p><p>如果需要测量代码执行时间或某个操作的持续时间，建议使用 time.perf_counter()，因为它提供更高的精度和更稳定的测量。<br>如果你需要获取当前系统时间，例如记录日志时间或时间戳，可以使用 time.time()，因为它表示的是绝对时间。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> time<br><br><span class="hljs-comment"># 使用 time.perf_counter() 测量时间间隔</span><br>start = time.perf_counter()<br><span class="hljs-comment"># 执行一些操作</span><br>end = time.perf_counter()<br><span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;操作耗时：<span class="hljs-subst">&#123;end - start&#125;</span> 秒&quot;</span>)<br><br><span class="hljs-comment"># 使用 time.time() 获取当前时间戳</span><br>current_time = time.time()<br><span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;当前UNIX时间戳：<span class="hljs-subst">&#123;current_time&#125;</span>&quot;</span>)<br></code></pre></td></tr></table></figure><p>总结：选择这两个函数的关键在于用途。如果目的是进行高精度的时间间隔测量，选用 time.perf_counter()。如果目的是获取系统的当前时间戳，选用 time.time()。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/10/29/time%E4%B8%AD%E7%9A%84time-perf-counter-%E5%92%8Ctime-time-%E5%AF%B9%E6%AF%94/</id>
    <link href="https://yuluod.github.io/2024/10/29/time%E4%B8%AD%E7%9A%84time-perf-counter-%E5%92%8Ctime-time-%E5%AF%B9%E6%AF%94/"/>
    <published>2024-10-29T01:44:13.000Z</published>
    <summary>
      <![CDATA[<p>简而言之 time 模块中的 time（）函数用于获取当前时间，perf_counter（）函数用于获取当前时间的精确计时器值，通常用于测量程序的执行时间。</p>]]>
    </summary>
    <title>time中的time.perf_counter()和time.time()对比</title>
    <updated>2024-10-29T01:48:52.677Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <category term="Flask" scheme="https://yuluod.github.io/tags/Flask/"/>
    <content>
      <![CDATA[<p>当Flask实现的接口，返回的数据中有中文存在的时候，会默认显示为Unicode编码。<br>这个就是ASCII码，用转换工具能够显示原来的。<br>之前可以使用如下方式解决</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ptyhon">app = Flask（__name__）<br>app.config［&#x27;JSON_AS_ASCII&#x27;］ = False ＃ 解决中文乱码问题<br></code></pre></td></tr></table></figure><p>但是最新的不行了</p><span id="more"></span><p>针对版本的检查，是新版本弃用上面的方法了，新版本的如下<br>Flask 2.3.0以上</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ptyhon">app = Flask（__name__）<br>app.json.ensure_ascii = False # 解决中文乱码问题<br></code></pre></td></tr></table></figure><p>Flask 2.2.5以下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ptyhon">app = Flask（__name__）<br>app.config［&#x27;JSON_AS_ASCII&#x27;］ = False # 解决中文乱码问题<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://yuluod.github.io/2024/09/12/%E8%A7%A3%E5%86%B3Flask%E8%BF%94%E5%9B%9E%E4%B8%AD%E6%96%87Unicode%E7%BC%96%E7%A0%81%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <link href="https://yuluod.github.io/2024/09/12/%E8%A7%A3%E5%86%B3Flask%E8%BF%94%E5%9B%9E%E4%B8%AD%E6%96%87Unicode%E7%BC%96%E7%A0%81%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <published>2024-09-12T02:10:26.000Z</published>
    <summary>
      <![CDATA[<p>当Flask实现的接口，返回的数据中有中文存在的时候，会默认显示为Unicode编码。<br>这个就是ASCII码，用转换工具能够显示原来的。<br>之前可以使用如下方式解决</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ptyhon">app = Flask（__name__）<br>app.config［&#x27;JSON_AS_ASCII&#x27;］ = False ＃ 解决中文乱码问题<br></code></pre></td></tr></table></figure>

<p>但是最新的不行了</p>]]>
    </summary>
    <title>解决Flask返回中文Unicode编码的问题</title>
    <updated>2024-09-12T02:15:03.953Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Life" scheme="https://yuluod.github.io/categories/Life/"/>
    <category term="Baby" scheme="https://yuluod.github.io/tags/Baby/"/>
    <content>
      <![CDATA[<p>从知道她有了的时候，两个人的诧异和紧张；</p><p>到孕期各种检查，身体不舒服；</p><p>到终于的生出来的那一刻；</p><p>不能说五味杂陈，只能说感慨颇多。</p><span id="more"></span><p>小家伙在肚子里面的时候，就是一个调皮的娃，各种扑腾。</p><p>家里人问我们起名字，我们商量好了，写几个名字，让小家伙到时候自己抓一个就行。</p><p>本来要等40周，可惜检查的时候医生说生长太慢了，就提前入院，检查，打针，等待，然后生产。好歹整个过程还算顺利。</p><p>等待的时候，护士问她要不要让我陪产，她说我害怕，就别让我进来了。我犹豫了半天，终于还是进去了。不知道怎么形容整个过程，她说我的汗比她还多。</p><p>生出来之后，医生没有让我剪脐带，但是登记好之后，让我抱抱小家伙。</p><p>一到手里，第一感觉，沉甸甸的小家伙。满身胎脂，眼睛看来看去，动来动去的，看着真的是让人开心，非常单纯得开心。</p><p>但其实到现在还是没感觉到什么父爱，问她也不知道啥叫母爱。小家伙从最开始哼哼唧唧，到后来饿得难受，哭得那个样子，看得人确实心疼。两个人在医院真的被小家伙折磨够呛，虽然已经算很乖的宝宝了，但是哭、闹、哼哼唧唧都让两个人没法彻底睡着。</p><p>都在慢慢学着哄娃，出生之前跟小家伙说，只希望你健康。</p><p>现在看着小家伙熟睡样子，加一句，希望你健康，也希望你快乐。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/06/12/%E5%B0%8F%E5%AE%B6%E4%BC%99%E7%BB%88%E4%BA%8E%E5%87%BA%E6%9D%A5%E4%BA%86/</id>
    <link href="https://yuluod.github.io/2024/06/12/%E5%B0%8F%E5%AE%B6%E4%BC%99%E7%BB%88%E4%BA%8E%E5%87%BA%E6%9D%A5%E4%BA%86/"/>
    <published>2024-06-12T08:47:45.000Z</published>
    <summary>
      <![CDATA[<p>从知道她有了的时候，两个人的诧异和紧张；</p>
<p>到孕期各种检查，身体不舒服；</p>
<p>到终于的生出来的那一刻；</p>
<p>不能说五味杂陈，只能说感慨颇多。</p>]]>
    </summary>
    <title>小家伙终于出来了</title>
    <updated>2024-09-12T02:44:40.498Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Front-Dev" scheme="https://yuluod.github.io/categories/Front-Dev/"/>
    <content>
      <![CDATA[<p>在HTTP请求中，Content-Type是一个HTTP头部字段，用于指示资源的MIME类型（媒体类型）。它在POST和PUT请求中特别重要，因为这些请求通常会发送数据给服务器，而Content-Type告诉服务器如何解析这些数据。以下是几种常见的Content-Type类型及其简要分析：</p><span id="more"></span><ol><li>text&#x2F;plain</li></ol><p>用途：这是最基本的MIME类型，用于传输未经格式化的纯文本。它可以用于发送简单的文本信息，比如纯文本的电子邮件。<br>特点：由于text&#x2F;plain类型的数据不包含任何文本格式（如字体、颜色、HTML标签等），它通常不适用于需要丰富格式的文本传输。这种格式的数据易于人类阅读，但不适合复杂的数据交互。</p><ol start="2"><li>text&#x2F;html</li></ol><p>用途：用于传输HTML格式的文档，这种类型的数据经常被Web服务器用来发送网页到浏览器。<br>特点：text&#x2F;html类型的数据可以包含HTML标签和脚本，这些标签和脚本使得文档具有结构化和交互性。浏览器会解析这些标签来渲染出用户可交互的网页。</p><ol start="3"><li>application&#x2F;json</li></ol><p>用途：用于传输JSON（JavaScript Object Notation）格式的数据。JSON是一种轻量级的数据交换格式，现在广泛用于网络上的数据交换和Web API。<br>特点：JSON格式易于人阅读和编写，同时也易于机器解析和生成。它基于JavaScript的对象字面量语法，但独立于语言，许多编程语言都有解析JSON的能力。</p><ol start="4"><li>application&#x2F;x-www-form-urlencoded</li></ol><p>用途：这是在提交HTML表单时最常用的内容类型。当表单提交时，表单内的数据会被编码为键值对，如key1&#x3D;value1&amp;key2&#x3D;value2。<br>特点：它是一种简单的编码机制，但不适合传输大型的二进制数据或非常复杂的结构化数据。</p><ol start="5"><li>multipart&#x2F;form-data</li></ol><p>用途：用于向服务器上传文件。在表单中使用enctype&#x3D;”multipart&#x2F;form-data”时，表单数据会被分割成多个部分，每个部分对应一个表单控件或文件的内容。<br>特点：这种类型特别适合传输文件或大量数据。每个部分都可以有自己的Content-Type，允许混合传输不同类型的数据。</p><ol start="6"><li>application&#x2F;xml 或 text&#x2F;xml</li></ol><p>用途：用于传输XML（Extensible Markup Language）格式的数据。XML是一种标记语言，用于编码文档以结构化的方式。<br>特点：XML文档是自描述的，可以包含它们自己的元数据。它们是可扩展的，允许定义自定义标签。XML被设计用来传输和存储数据，具有很强的自描述性和可扩展性。</p><ol start="7"><li>application&#x2F;octet-stream</li></ol><p>用途：用于传输二进制流数据。这种类型通常用于下载文件或发送软件和二进制文件。<br>特点：由于application&#x2F;octet-stream是一个通用的二进制格式，客户端通常不会尝试解析它，而是直接下载或通过特定应用程序处理。这使得它非常适合传输不应该被直接显示的文件或数据。<br>选择正确的Content-Type对于确保HTTP请求和响应被正确解析和处理是至关重要的。服务器和客户端都依赖这个头部信息来确定如何处理传输的数据。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/03/22/Content-Type%E7%9A%84%E4%B8%8D%E5%90%8C%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%88%AB/</id>
    <link href="https://yuluod.github.io/2024/03/22/Content-Type%E7%9A%84%E4%B8%8D%E5%90%8C%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%88%AB/"/>
    <published>2024-03-22T14:06:52.000Z</published>
    <summary>
      <![CDATA[<p>在HTTP请求中，Content-Type是一个HTTP头部字段，用于指示资源的MIME类型（媒体类型）。它在POST和PUT请求中特别重要，因为这些请求通常会发送数据给服务器，而Content-Type告诉服务器如何解析这些数据。以下是几种常见的Content-Type类型及其简要分析：</p>]]>
    </summary>
    <title>Content-Type的不同类型的区别</title>
    <updated>2024-03-22T14:07:52.692Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Python" scheme="https://yuluod.github.io/categories/Python/"/>
    <category term="Python" scheme="https://yuluod.github.io/tags/Python/"/>
    <content>
      <![CDATA[<p>需要 Python 虚拟环境管理工具的原因有很多，其中包括：</p><p>• 隔离项目依赖项： 每个项目可能需要不同的库和 Python 版本。通过使用虚拟环境，可以将每个项目的依赖项隔离在自己的环境中，避免冲突和版本问题。<br>• 提高开发效率： 虚拟环境可以让你快速地在不同的项目之间切换，而无需担心依赖项冲突或版本问题。<br>• 简化部署： 如果你将项目部署到不同的服务器上，那么你可以使用虚拟环境来确保每个服务器上都安装了正确的依赖项。<br>• 提高安全性： 虚拟环境可以帮助开发者隔离不安全的代码或库，防止它们影响其他项目或系统。</p><p>现在Python环境有三个用的比较多的，分别是<br>• virtualenv<br>• ven<br>• pipenv</p><span id="more"></span><h3 id="联系"><a href="#联系" class="headerlink" title="联系"></a>联系</h3><p>• virtualenv、venv 和 pipenv 都是 Python 的虚拟环境管理工具。<br>• 它们都可以用来创建和管理独立的 Python 环境，可以在不同的项目中使用不同的 Python版本和库。</p><h3 id="区别"><a href="#区别" class="headerlink" title="区别"></a>区别</h3><p>• virtualenv 是最古老的 Python 虚拟环境管理工具。它需要开发者明确地创建和管理虚拟环境。<br>• venv 是 Python 3.3 中内置的虚拟环境管理工具。它比 virtualenv 更易于使用，因为它不需要你明确地创建和管理虚拟环境。<br>• pipenv 是一个更高级的 Python 虚拟环境管理工具。它不仅可以创建和管理虚拟环境，还可以管理项目依赖项。</p><p>表格对比：</p><table><thead><tr><th>特性</th><th align="left">virtualenv</th><th>venv</th><th>pipenv</th></tr></thead><tbody><tr><td>创建时间</td><td align="left">2007</td><td>2013</td><td>2018</td></tr><tr><td>内置于 Python 中</td><td align="left">否</td><td>是</td><td>否</td></tr><tr><td>管理依赖项</td><td align="left">否</td><td>否</td><td>是</td></tr><tr><td>易用性</td><td align="left">较难</td><td>较容易</td><td>最容易</td></tr></tbody></table><p>总体来说，如果只是想创建一个独立的 Python 环境，那么 venv 是一个不错的选择。如果想管理项目依赖项，那么 pipenv 是一个更好的选择。</p><h3 id="何时使用"><a href="#何时使用" class="headerlink" title="何时使用"></a>何时使用</h3><p>何时使用 virtualenv、venv 或 pipenv？</p><p>• 使用 virtualenv：<br>    • 需要对虚拟环境有更精细的控制。<br>    • 需要在旧版本的 Python 上创建虚拟环境。<br>• 使用 venv：<br>    • 想轻松地创建和管理虚拟环境。<br>    • 不需要管理项目依赖项。<br>• 使用 pipenv：<br>    • 想管理项目依赖项。<br>    • 想要一个更易于使用的虚拟环境管理工具。</p><p>请注意，pipenv 仍然是一个相对较新的工具，可能不如 virtualenv 和 venv 稳定。但是，它具有许多有用的特性，并且正在迅速发展。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/02/04/Python%E4%B8%8Bvirtualenv-venv-pipenv-%E4%B9%8B%E9%97%B4%E7%9A%84%E8%81%94%E7%B3%BB%E4%B8%8E%E5%8C%BA%E5%88%AB/</id>
    <link href="https://yuluod.github.io/2024/02/04/Python%E4%B8%8Bvirtualenv-venv-pipenv-%E4%B9%8B%E9%97%B4%E7%9A%84%E8%81%94%E7%B3%BB%E4%B8%8E%E5%8C%BA%E5%88%AB/"/>
    <published>2024-02-04T07:36:32.000Z</published>
    <summary>
      <![CDATA[<p>需要 Python 虚拟环境管理工具的原因有很多，其中包括：</p>
<p>• 隔离项目依赖项： 每个项目可能需要不同的库和 Python 版本。通过使用虚拟环境，可以将每个项目的依赖项隔离在自己的环境中，避免冲突和版本问题。<br>• 提高开发效率： 虚拟环境可以让你快速地在不同的项目之间切换，而无需担心依赖项冲突或版本问题。<br>• 简化部署： 如果你将项目部署到不同的服务器上，那么你可以使用虚拟环境来确保每个服务器上都安装了正确的依赖项。<br>• 提高安全性： 虚拟环境可以帮助开发者隔离不安全的代码或库，防止它们影响其他项目或系统。</p>
<p>现在Python环境有三个用的比较多的，分别是<br>• virtualenv<br>• ven<br>• pipenv</p>]]>
    </summary>
    <title>
      <![CDATA[Python下virtualenv & venv & pipenv 之间的联系与区别]]>
    </title>
    <updated>2024-02-04T07:41:40.295Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Front-Dev" scheme="https://yuluod.github.io/categories/Front-Dev/"/>
    <category term="npm" scheme="https://yuluod.github.io/tags/npm/"/>
    <content>
      <![CDATA[<p>npm 的服务在国外，大陆的服务器有时会拉取模块不顺畅。同时阿里云在大陆建立了一个 npmmirror 镜像站。现在有两种方式可以进行设置：</p><span id="more"></span><p>方式一：临时使用</p><ol><li>在终端中输入以下命令：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">npm --registry https://registry.npmmirror.com install [模块名]<br></code></pre></td></tr></table></figure><p>如果安装成功，则表示设置成功。</p><p>方式二：修改本地配置，永久生效</p><ol><li>在终端中输入以下命令：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">npm config set registry https://registry.npmmirror.com<br></code></pre></td></tr></table></figure><ol start="2"><li>输入以下命令确认设置是否成功：</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">npm config get registry<br></code></pre></td></tr></table></figure><p>如果输出结果为 <code>https://registry.npmmirror.com</code>，则表示设置成功。</p><p>无论使用哪种方式，都可以解决大陆服务器有时会拉取模块不通畅的问题。</p><p>注意：</p><p>• 如果需要恢复到默认的 npm 源，可以使用以下命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">npm config set registry https://registry.npmjs.org<br></code></pre></td></tr></table></figure><p><a href="https://npmmirror.com/">npmmirror 镜像站</a></p>]]>
    </content>
    <id>https://yuluod.github.io/2024/02/03/%E4%BF%AE%E6%94%B9NPM%E9%95%9C%E5%83%8F%E6%BA%90/</id>
    <link href="https://yuluod.github.io/2024/02/03/%E4%BF%AE%E6%94%B9NPM%E9%95%9C%E5%83%8F%E6%BA%90/"/>
    <published>2024-02-03T13:59:48.000Z</published>
    <summary>
      <![CDATA[<p>npm 的服务在国外，大陆的服务器有时会拉取模块不顺畅。同时阿里云在大陆建立了一个 npmmirror 镜像站。现在有两种方式可以进行设置：</p>]]>
    </summary>
    <title>修改NPM镜像源</title>
    <updated>2024-02-03T14:43:08.049Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="NAS" scheme="https://yuluod.github.io/categories/NAS/"/>
    <category term="unRAID" scheme="https://yuluod.github.io/tags/unRAID/"/>
    <content>
      <![CDATA[<p>既然unRAID支持Docker，那么可以直接使用这个安装迅雷插件，但是自带应用商店的版本已经很久了，所以我们需要手动安装。</p><span id="more"></span><p>点击添加容器<br><img src="https://s2.loli.net/2024/01/17/Namo75DgEyMCkWV.jpg"></p><p>源：cnk3x&#x2F;xunlei:latest</p><p>网络类型选择bridge</p><p>添加data和downloads两个映射目录，和2345端口</p><p><strong>重点：要开启“特权”</strong></p><p>否则会报错，具体的如下：<br><img src="https://s2.loli.net/2024/01/17/AoijQD1EyLrUCYR.jpg"></p><p>如果还想希望加上图标，则按图点击<br><img src="https://s2.loli.net/2024/01/17/tZHoe7UzCQN5OlE.jpg"></p><p>然后在图标网址填写如下：<br><a href="https://cdn.jsdelivr.net/gh/xushier/HD-Icons@master/border-radius/Xunlei_A.png">https://cdn.jsdelivr.net/gh/xushier/HD-Icons@master/border-radius/Xunlei_A.png</a></p>]]>
    </content>
    <id>https://yuluod.github.io/2024/01/17/unRAID%E5%AE%89%E8%A3%85Docker%E8%BF%85%E9%9B%B7%E6%8F%92%E4%BB%B6/</id>
    <link href="https://yuluod.github.io/2024/01/17/unRAID%E5%AE%89%E8%A3%85Docker%E8%BF%85%E9%9B%B7%E6%8F%92%E4%BB%B6/"/>
    <published>2024-01-17T03:26:50.000Z</published>
    <summary>
      <![CDATA[<p>既然unRAID支持Docker，那么可以直接使用这个安装迅雷插件，但是自带应用商店的版本已经很久了，所以我们需要手动安装。</p>]]>
    </summary>
    <title>unRAID安装Docker迅雷插件</title>
    <updated>2024-01-17T03:28:49.190Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="NAS" scheme="https://yuluod.github.io/categories/NAS/"/>
    <category term="unRAID" scheme="https://yuluod.github.io/tags/unRAID/"/>
    <content>
      <![CDATA[<p>unRAID 是一个近年来备受欢迎的 NAS 系统，其原生支持 Docker，因此在安装和管理 Docker 应用方面非常方便，这也是 unRAID 受欢迎的原因之一。<br>在NAS上，tmm是用来刮削很方便的一个软件，无论是mac还是windows，但是由于还需要额外打开软件，终究还是不如直接运行在unRAID上方便。</p><span id="more"></span><h2 id="在哪里安装Docker"><a href="#在哪里安装Docker" class="headerlink" title="在哪里安装Docker"></a>在哪里安装Docker</h2><p>unRAID 提供了一个叫做 APPS全称叫做 Community Applications（社区应用，简称为 CA），用户可以在里面搜索到相应的插件、Docker 等进行安装。<br><img src="https://s2.loli.net/2024/01/17/RcLdF8XDCPeHsNp.jpg" alt="安装docker"></p><p>直接在里面搜索TinyMediaManager ，但是发现只有两个镜像，一个还是v3版本的，所以我们需要手动安装</p><h2 id="手动安装"><a href="#手动安装" class="headerlink" title="手动安装"></a>手动安装</h2><p>可以点击右侧的“单击此处以从 DockerHub 获取更多结果”来展示 Docker Hub 所搜索到的内容：<br><img src="https://s2.loli.net/2024/01/17/xFVh9OLIT3kbYR7.jpg"><br>当你点击“安装”按钮之后，unRAID 会先拉取 Docker 镜像，然后会尝试自动给你添加相应的参数：</p><p><img src="https://s2.loli.net/2024/01/17/clpkIC1mreGDNo9.jpg"></p><h2 id="推荐配置"><a href="#推荐配置" class="headerlink" title="推荐配置"></a>推荐配置</h2><p>此处直接推荐dzhuang的镜像<a href="https://github.com/dzhuang/tinymediamanager4-docker">tinymediamanager4-docker</a> ，不过V4介绍说明有点错误，详情直接参考下图<br><img src="https://s2.loli.net/2024/01/17/Ds9ucZnJzKqOfi4.jpg"></p><h2 id="自定义图标"><a href="#自定义图标" class="headerlink" title="自定义图标"></a>自定义图标</h2><p>编辑Docker镜像，然后点击如图位置<br><img src="https://s2.loli.net/2024/01/17/xbsvO1nTCMHJfBj.jpg"></p><p>图标我选择的是如下：<a href="https://cdn.jsdelivr.net/gh/xushier/HD-Icons@master/border-radius/Tmm_A.png">https://cdn.jsdelivr.net/gh/xushier/HD-Icons@master/border-radius/Tmm_A.png</a><br>如果不喜欢，可以在这个地址下找到自己喜欢的：<a href="https://github.com/xushier/HD-Icons">GitHub - xushier&#x2F;HD-Icons: 高清仪表盘图标（1024x1024 分辨率）</a><br>然后点击最下面的确定即可。</p>]]>
    </content>
    <id>https://yuluod.github.io/2024/01/17/%E5%9C%A8unRAID%E4%B8%AD%E8%87%AA%E5%AE%9A%E4%B9%89%E5%AE%89%E8%A3%85tmm%E7%9A%84Docker%E9%95%9C%E5%83%8F/</id>
    <link href="https://yuluod.github.io/2024/01/17/%E5%9C%A8unRAID%E4%B8%AD%E8%87%AA%E5%AE%9A%E4%B9%89%E5%AE%89%E8%A3%85tmm%E7%9A%84Docker%E9%95%9C%E5%83%8F/"/>
    <published>2024-01-17T02:40:17.000Z</published>
    <summary>
      <![CDATA[<p>unRAID 是一个近年来备受欢迎的 NAS 系统，其原生支持 Docker，因此在安装和管理 Docker 应用方面非常方便，这也是 unRAID 受欢迎的原因之一。<br>在NAS上，tmm是用来刮削很方便的一个软件，无论是mac还是windows，但是由于还需要额外打开软件，终究还是不如直接运行在unRAID上方便。</p>]]>
    </summary>
    <title>在unRAID中自定义安装tmm的Docker镜像</title>
    <updated>2024-01-17T02:44:45.617Z</updated>
  </entry>
  <entry>
    <author>
      <name>yuluo</name>
    </author>
    <category term="Life" scheme="https://yuluod.github.io/categories/Life/"/>
    <content>
      <![CDATA[<p>一年好像没做成什么事</p><p>多了太多意外</p><p>看着跨年演唱会</p><p>一个个熟悉的歌手</p><p>老去的样子</p><p>时间真的是一把刀</p><p>一刀刀把时间刻成回忆</p><p>对镜抑是老去</p><p>岁月如你</p><span id="more"></span>]]>
    </content>
    <id>https://yuluod.github.io/2023/12/31/2023%E7%9A%84%E7%BB%93%E6%9D%9F/</id>
    <link href="https://yuluod.github.io/2023/12/31/2023%E7%9A%84%E7%BB%93%E6%9D%9F/"/>
    <published>2023-12-31T12:33:52.000Z</published>
    <summary>
      <![CDATA[<p>一年好像没做成什么事</p>
<p>多了太多意外</p>
<p>看着跨年演唱会</p>
<p>一个个熟悉的歌手</p>
<p>老去的样子</p>
<p>时间真的是一把刀</p>
<p>一刀刀把时间刻成回忆</p>
<p>对镜抑是老去</p>
<p>岁月如你</p>]]>
    </summary>
    <title>2023的结束</title>
    <updated>2023-12-31T12:52:29.775Z</updated>
  </entry>
</feed>
