<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-cn</language>
    <copyright>Copyright 2026 ferstar</copyright>
    <lastBuildDate>Thu, 22 Jan 2026 14:30:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>万万没想到 6202 年了我还用 crontab &#43; HTTP Header 同步时钟</title>
      <link>https://blog.ferstar.org/posts/crontab-http-header-time-sync/</link>
      <pubDate>Thu, 22 Jan 2026 14:30:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/crontab-http-header-time-sync/</guid>
      <description>隔离环境 UDP 被拦截导致 NTP 失效，Prometheus 报错时间偏差 38 秒，Teleport 握手失败。最终用 HTTP Date Header + Crontab 定时任务手动对齐时钟。</description><content:encoded><![CDATA[
<h2 class="relative group">事情是这样的
    <div id="事情是这样的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%ba%8b%e6%83%85%e6%98%af%e8%bf%99%e6%a0%b7%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>2026 年 1 月 21 日，目标集群 集群告警了。</p>
<p>Prometheus 报错：<code>Warning: Error fetching server time: Detected 38.116000175476074 seconds time difference between your browser and the server.</code> 38 秒，说多不多说少不少，但足够让 Teleport 的安全校验挂掉。</p>
<p>更离谱的是，target02 直接失联了——Teleport 握手失败，根本连不上 SSH。</p>

<h2 class="relative group">排查过程
    <div id="排查过程" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">第一步：以为是网络问题
    <div id="第一步以为是网络问题" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ac%ac%e4%b8%80%e6%ad%a5%e4%bb%a5%e4%b8%ba%e6%98%af%e7%bd%91%e7%bb%9c%e9%97%ae%e9%a2%98" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>在 target01 上探测跳板机的连通性：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># TCP 扫描（正常）</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> port in <span class="m">22</span> <span class="m">80</span> 8888<span class="p">;</span> <span class="k">do</span> nc -zv -w <span class="m">2</span> 100.64.0.5 <span class="nv">$port</span><span class="p">;</span> <span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># UDP 扫描（有猫腻）</span>
</span></span><span class="line"><span class="cl">nc -uvz -w <span class="m">2</span> 100.64.0.5 <span class="m">8888</span>  <span class="c1"># 显示 success，但收不到回程包</span></span></span></code></pre></div></div>
<p>TCP 通，UDP 看起来通但数据包丢了。熟悉的配方，隔离环境的老朋友们都知道这意味着什么。</p>

<h3 class="relative group">第二步：抓包确认
    <div id="第二步抓包确认" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e6%8a%93%e5%8c%85%e7%a1%ae%e8%ae%a4" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>在跳板机（jump-host）开启抓包，同时从 target01 发送 UDP 包：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># jump-host 执行</span>
</span></span><span class="line"><span class="cl">sudo tcpdump -i any udp port <span class="m">8888</span> -n
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># target01 执行</span>
</span></span><span class="line"><span class="cl">nc -u 100.64.0.5 <span class="m">8888</span> <span class="o"><<<</span> <span class="s2">"test"</span></span></span></code></pre></div></div>
<p>结果：跳板机侧完全没有捕获到任何 UDP 包。UDP 在隔离网络层被拦截了。</p>

<h3 class="relative group">第三步：检查代理
    <div id="第三步检查代理" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ac%ac%e4%b8%89%e6%ad%a5%e6%a3%80%e6%9f%a5%e4%bb%a3%e7%90%86" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -v http://100.64.0.5:8888
</span></span><span class="line"><span class="cl"><span class="c1"># 响应：< Proxy-Agent: gost/2.12.0</span></span></span></code></pre></div></div>
<p>8888 端口是 gost 代理，但它主要处理 TCP 隧道。UDP？这儿不接待。</p>

<h2 class="relative group">真正的凶手：时钟漂移
    <div id="真正的凶手时钟漂移" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%9c%9f%e6%ad%a3%e7%9a%84%e5%87%b6%e6%89%8b%e6%97%b6%e9%92%9f%e6%bc%82%e7%a7%bb" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>绕了一圈，问题根源浮出水面——目标节点的时钟已经飞了：</p>
<ul>
<li><strong>target01</strong>：时钟滞后约 38 秒，Prometheus 图表数据断点、查询偏移</li>
<li><strong>target02</strong>：时钟偏移更严重，Teleport 安全校验直接 fail</li>
</ul>
<p>没有 NTP（隔离环境 UDP 114 端口通不过），chrony 也在那儿干瞪眼。</p>

<h2 class="relative group">解决方案：HTTP Date Header 手动同步
    <div id="解决方案http-date-header-手动同步" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88http-date-header-%e6%89%8b%e5%8a%a8%e5%90%8c%e6%ad%a5" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">步骤一：禁用失效的 chrony
    <div id="步骤一禁用失效的-chrony" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%ad%a5%e9%aa%a4%e4%b8%80%e7%a6%81%e7%94%a8%e5%a4%b1%e6%95%88%e7%9a%84-chrony" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl stop chronyd
</span></span><span class="line"><span class="cl">systemctl disable chronyd</span></span></code></pre></div></div>

<h3 class="relative group">步骤二：手动对齐时钟
    <div id="步骤二手动对齐时钟" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%ad%a5%e9%aa%a4%e4%ba%8c%e6%89%8b%e5%8a%a8%e5%af%b9%e9%bd%90%e6%97%b6%e9%92%9f" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>跳板机 80 端口的 HTTP 响应头里有个 <code>Date</code> 字段，虽然精度是秒级，但总比没有强：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 提取 HTTP Header 中的 Date 字段并设置系统时间</span>
</span></span><span class="line"><span class="cl"><span class="nv">HTTP_DATE</span><span class="o">=</span><span class="k">$(</span>curl -sI http://100.64.0.5 <span class="p">|</span> grep -i <span class="s2">"^Date:"</span> <span class="p">|</span> cut -d<span class="s2">" "</span> -f2-<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -n <span class="s2">"</span><span class="nv">$HTTP_DATE</span><span class="s2">"</span> <span class="o">]</span> <span class="o">&&</span> date -s <span class="s2">"</span><span class="nv">$HTTP_DATE</span><span class="s2">"</span></span></span></code></pre></div></div>
<p>没错，2026 年了，我还在用 <code>curl</code> + <code>date -s</code> 同步时钟。上次这么干还是在 OpenWrt 上，那已经是十多年前的事了。</p>

<h3 class="relative group">步骤三：定时任务持久化
    <div id="步骤三定时任务持久化" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%ad%a5%e9%aa%a4%e4%b8%89%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1%e6%8c%81%e4%b9%85%e5%8c%96" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>在 target01 和 target02 上配置每小时自动同步：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">(</span>crontab -l 2>/dev/null<span class="p">;</span> <span class="nb">echo</span> <span class="s1">'0 * * * * HTTP_DATE=$(curl -sI http://100.64.0.5 | grep -i "^Date:" | cut -d" " -f2-) && [ -n "$HTTP_DATE" ] && date -s "$HTTP_DATE"'</span><span class="o">)</span> <span class="p">|</span> crontab -</span></span></code></pre></div></div>

<h2 class="relative group">教训
    <div id="教训" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%95%99%e8%ae%ad" aria-label="锚点">#</a>
    </span>
    
</h2>
<ol>
<li>
<p><strong>隔离环境没有 NTP 是常态</strong>。UDP 被拦截时，HTTP 是最后的安全通道。</p>
</li>
<li>
<p><strong>非标端口的陷阱</strong>。8888 虽然在 gost 中配置了，但 目标 客户端没同步配置隧道，直接访问这个 UDP 端口必然失败。</p>
</li>
<li>
<p><strong>Prometheus 对时间极度敏感</strong>。大规模集群里，时间同步的鲁棒性比想象中重要得多。</p>
</li>
</ol>
<hr>
<p>事后回想这套流程：禁用 chrony → curl HTTP Header → date 设置 → crontab 定时。整个链路充满了"能跑就行"的务实感。</p>
<p>下次再有隔离环境部署，或许该考虑把 HTTP Date 同步做成 systemd timer 服务？</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>KVM GPU 直通与单节点 K8s 监控落地记录</title>
      <link>https://blog.ferstar.org/posts/kvm-gpu-passthrough-k8s-monitoring/</link>
      <pubDate>Thu, 22 Jan 2026 10:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/kvm-gpu-passthrough-k8s-monitoring/</guid>
      <description>GPU 直通后黑屏且部分卡不可用 + 调整 OVMF/PCI hole64/NUMA 并完善监控链路 + 8 卡 NVSwitch 与指标采集稳定</description><content:encoded><![CDATA[<p>这是一篇面向实操的记录，按当时的故障路径整理关键修复点、配置片段与验证方式，避免下次重复踩坑。</p>

<h2 class="relative group">场景与目标
    <div id="场景与目标" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9c%ba%e6%99%af%e4%b8%8e%e7%9b%ae%e6%a0%87" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li>宿主机：GPU 节点，虚拟机：<code>ubuntu_gpu</code>（KVM/libvirt, UEFI/OVMF）</li>
<li>目标：8 卡 H800 + 4 NVSwitch 直通，单节点 K8s + GPU Operator + Prometheus，可采集 DCGM 指标</li>
</ul>

<h2 class="relative group">故障现象与修复路径
    <div id="故障现象与修复路径" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1%e4%b8%8e%e4%bf%ae%e5%a4%8d%e8%b7%af%e5%be%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<pre class="not-prose mermaid">flowchart TD
  A[VM 启动 + GPU 直通] --> B[黑屏 / 部分 GPU 无法初始化]
  B --> C[切换 OVMF 非 Secure Boot]
  C --> D[扩大 PCI hole64]
  D --> E[NUMA + vCPU 亲和]
  E --> F[8 GPU + NVSwitch 正常]
  F --> G[K8s + GPU Operator + Prometheus]
  G --> H[DCGM 指标验证通过]</pre>

<h3 class="relative group">1) 黑屏与 PCI 资源不足
    <div id="1-黑屏与-pci-资源不足" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e9%bb%91%e5%b1%8f%e4%b8%8e-pci-%e8%b5%84%e6%ba%90%e4%b8%8d%e8%b6%b3" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li>现象：GPU 直通后 VNC 无画面，驱动报 <code>PCI I/O region invalid</code></li>
<li>修复：切换 OVMF non-secure，扩大 PCI hole64</li>
</ul>
<p>关键片段：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c"><!-- OVMF non-secure --></span>
</span></span><span class="line"><span class="cl"><span class="nt"><loader</span> <span class="na">readonly=</span><span class="s">'yes'</span> <span class="na">type=</span><span class="s">'pflash'</span><span class="nt">></span>/usr/share/edk2/ovmf/OVMF_CODE.cc.fd<span class="nt"></loader></span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"><!-- Q35 PCIe 64-bit hole --></span>
</span></span><span class="line"><span class="cl"><span class="nt"><qemu:commandline></span>
</span></span><span class="line"><span class="cl">  <span class="nt"><qemu:arg</span> <span class="na">value=</span><span class="s">'-global'</span><span class="nt">/></span>
</span></span><span class="line"><span class="cl">  <span class="nt"><qemu:arg</span> <span class="na">value=</span><span class="s">'q35-pcihost.pci-hole64-size=2048G'</span><span class="nt">/></span>
</span></span><span class="line"><span class="cl"><span class="nt"></qemu:commandline></span></span></span></code></pre></div></div>

<h3 class="relative group">2) IP 不通（DHCP 绑定）
    <div id="2-ip-不通dhcp-绑定" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-ip-%e4%b8%8d%e9%80%9adhcp-%e7%bb%91%e5%ae%9a" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li>原因：网卡 MAC 改动导致 DHCP 租约不匹配</li>
<li>修复：恢复旧 MAC，IP 回到 <code>192.168.122.146</code></li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 旧 MAC</span>
</span></span><span class="line"><span class="cl">52:54:00:a9:a2:11</span></span></code></pre></div></div>

<h3 class="relative group">3) 资源调整（内存 + NUMA）
    <div id="3-资源调整内存--numa" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e8%b5%84%e6%ba%90%e8%b0%83%e6%95%b4%e5%86%85%e5%ad%98--numa" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li>内存调整到 256GB</li>
<li>vCPU 分两组 pin 到对应 NUMA node，GPU 也按 NUMA 归属放置</li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt"><memory</span> <span class="na">unit=</span><span class="s">'KiB'</span><span class="nt">></span>268435456<span class="nt"></memory></span>
</span></span><span class="line"><span class="cl"><span class="nt"><currentMemory</span> <span class="na">unit=</span><span class="s">'KiB'</span><span class="nt">></span>268435456<span class="nt"></currentMemory></span></span></span></code></pre></div></div>

<h2 class="relative group">验证方式（GPU / K8s / 监控）
    <div id="验证方式gpu--k8s--监控" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%aa%8c%e8%af%81%e6%96%b9%e5%bc%8fgpu--k8s--%e7%9b%91%e6%8e%a7" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>GPU：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nvidia-smi -L
</span></span><span class="line"><span class="cl">nvidia-smi topo -m
</span></span><span class="line"><span class="cl">nvidia-smi nvlink -s</span></span></code></pre></div></div>
<p>K8s：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get nodes -o wide
</span></span><span class="line"><span class="cl">kubectl get pods -A</span></span></code></pre></div></div>
<p>Prometheus（DCGM 指标）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://127.0.0.1:9090/api/v1/query?query<span class="o">=</span>DCGM_FI_DEV_SM_CLOCK</span></span></code></pre></div></div>

<h2 class="relative group">关键配置片段
    <div id="关键配置片段" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%b3%e9%94%ae%e9%85%8d%e7%bd%ae%e7%89%87%e6%ae%b5" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">containerd 代理（无外网）
    <div id="containerd-代理无外网" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#containerd-%e4%bb%a3%e7%90%86%e6%97%a0%e5%a4%96%e7%bd%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/containerd.service.d/http-proxy.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"HTTP_PROXY=http://100.64.0.5:8888"</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"HTTPS_PROXY=http://100.64.0.5:8888"</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"NO_PROXY=127.0.0.1,localhost,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10"</span></span></span></code></pre></div></div>

<h3 class="relative group">GPU Operator + Prometheus
    <div id="gpu-operator--prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gpu-operator--prometheus" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm upgrade --install gpu-operator nvidia/gpu-operator <span class="se">\
</span></span></span><span class="line"><span class="cl">  -n gpu-operator --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set driver.enabled<span class="o">=</span><span class="nb">false</span> --set dcgmExporter.enabled<span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack <span class="se">\
</span></span></span><span class="line"><span class="cl">  -n monitoring --create-namespace</span></span></code></pre></div></div>

<h3 class="relative group">DCGM 指标接入 Prometheus
    <div id="dcgm-指标接入-prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#dcgm-%e6%8c%87%e6%a0%87%e6%8e%a5%e5%85%a5-prometheus" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ServiceMonitor 需要有与 Prometheus release 一致的标签</span>
</span></span><span class="line"><span class="cl">kubectl -n gpu-operator label servicemonitor nvidia-dcgm-exporter <span class="nv">release</span><span class="o">=</span>kube-prometheus-stack --overwrite</span></span></code></pre></div></div>

<h2 class="relative group">小结（可复用清单）
    <div id="小结可复用清单" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b0%8f%e7%bb%93%e5%8f%af%e5%a4%8d%e7%94%a8%e6%b8%85%e5%8d%95" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li>PCIe 资源不足先看 <code>pci-hole64-size</code>，这步最容易被忽略</li>
<li>OVMF Secure Boot 会干扰多卡启动，先用非 Secure Boot 验证</li>
<li>DHCP 租约异常时，先检查 MAC 是否被改过</li>
<li>监控链路：DCGM Exporter 必须匹配 Prometheus 的 <code>release</code> 标签</li>
</ul>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>VPS 迁移与性能压榨手册 (Debian 13 &#43; XanMod &#43; 443 端口复用)</title>
      <link>https://blog.ferstar.org/posts/vps-migration-and-optimization-guide/</link>
      <pubDate>Tue, 20 Jan 2026 10:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/vps-migration-and-optimization-guide/</guid>
      <description>小内存 VPS 迁移后性能吃紧，借助 XanMod、内核/内存调优与 443 端口复用方案，低配也能稳定跑服务。</description><content:encoded><![CDATA[<p>本文档记录了从 DigitalOcean 旧主机 (Ubuntu 20.04) 到新主机 (Debian 13 + XanMod) 的迁移过程及调优细节。</p>
<p>起因是原本 $6/mo 的实例（1GB RAM / 20GB DISK / 1TB BW）资源长期闲置，于是决定顺应“消费降级”的大潮，降配至 $4/mo 方案（512MB RAM / 10GB DISK / 500GB BW），怒省 1/3 的开销，性价比瞬间拉满。</p>
<p>如今博客已托管至“赛博菩萨” Cloudflare Pages，这台 512MB 的小鸡便能卸下重担，转而专职负责备用梯子和吃灰的公众号后台。虽然内存配置直接腰斩，让资源变得捉襟见肘，但在一番极限性能压榨下，这台小机器跑起来依然稳如老狗。</p>

<h2 class="relative group">1. 基础环境
    <div id="1-基础环境" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e5%9f%ba%e7%a1%80%e7%8e%af%e5%a2%83" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li><strong>源主机 (Source)</strong>: DigitalOcean Ubuntu 20.04 (IP 已隐藏)</li>
<li><strong>新主机 (Target)</strong>: DigitalOcean Debian 13 Trixie (IP 已隐藏)</li>
<li><strong>Reserved IP</strong>: 已挂载至新主机，用于 DNS 解析。</li>
<li><strong>Hostname / PTR</strong>: <code>ferstar.org</code> (通过重命名 DigitalOcean Droplet 自动触发 PTR 生成)。</li>
</ul>

<h2 class="relative group">2. 内核与内存优化 (Kernel 6.18+)
    <div id="2-内核与内存优化-kernel-618" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e5%86%85%e6%a0%b8%e4%b8%8e%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96-kernel-618" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">2.1 升级 XanMod Edge
    <div id="21-升级-xanmod-edge" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#21-%e5%8d%87%e7%ba%a7-xanmod-edge" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>安装具备 BBRv3 和最新调度特性的内核：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget -qO - https://dl.xanmod.org/archive.key <span class="p">|</span> gpg --dearmor <span class="p">|</span> tee /usr/share/keyrings/xanmod-archive-keyring.gpg > /dev/null
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] http://deb.xanmod.org releases main'</span> <span class="p">|</span> tee /etc/apt/sources.list.d/xanmod-kernel.list
</span></span><span class="line"><span class="cl">apt update <span class="o">&&</span> apt install linux-xanmod-edge-x64v3 -y</span></span></code></pre></div></div>
<p>注意：<code>linux-xanmod-edge-x64v3</code> 需要 CPU 支持 x86-64-v3 指令集，若不支持可改用 <code>linux-xanmod-edge-x64v2</code> 或 <code>linux-xanmod-edge-x64</code>。</p>

<h3 class="relative group">2.2 内存压榨与持久化 (zswap + MGLRU + KSM)
    <div id="22-内存压榨与持久化-zswap--mglru--ksm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#22-%e5%86%85%e5%ad%98%e5%8e%8b%e6%a6%a8%e4%b8%8e%e6%8c%81%e4%b9%85%e5%8c%96-zswap--mglru--ksm" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>针对 512MB RAM 的极限优化。由于部分内核参数不支持 <code>sysctl</code> 直接持久化，统一通过 <code>crontab</code> 的 <code>@reboot</code> 机制在开机时强制注入：</p>
<p><strong>持久化指令 (<code>crontab -e</code>):</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 开启 MGLRU 所有优化层，显著降低小内存下的 OOM 风险</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> <span class="m">7</span> > /sys/kernel/mm/lru_gen/enabled
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 开启 KSM 内存页合并，减少 Docker 容器间的重复内存占用</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> <span class="m">1</span> > /sys/kernel/mm/ksm/run
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 开启 zswap 收缩器，允许内核更积极地在压缩区与物理 Swap 间平衡数据</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> Y > /sys/module/zswap/parameters/shrinker_enabled</span></span></code></pre></div></div>
<p><strong>其他参数:</strong></p>
<ul>
<li><strong>zswap</strong>: 开启内存压缩缓存，当前使用 <strong>lzo</strong> 算法。</li>
<li><strong>Swap</strong>: 1GB 物理文件兜底。</li>
</ul>

<h2 class="relative group">3. 网络架构：443 端口全能复用 (SNI Proxy)
    <div id="3-网络架构443-端口全能复用-sni-proxy" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e7%bd%91%e7%bb%9c%e6%9e%b6%e6%9e%84443-%e7%ab%af%e5%8f%a3%e5%85%a8%e8%83%bd%e5%a4%8d%e7%94%a8-sni-proxy" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>利用 Nginx 1.29.4 (Mainline) 的 <code>stream</code> 模块实现基于域名的流量分流：</p>

<h3 class="relative group">3.1 Nginx 全局配置 (<code>/etc/nginx/nginx.conf</code>)
    <div id="31-nginx-全局配置-etcnginxnginxconf" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#31-nginx-%e5%85%a8%e5%b1%80%e9%85%8d%e7%bd%ae-etcnginxnginxconf" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">stream</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">map</span> <span class="nv">$ssl_preread_server_name</span> <span class="nv">$stream_map</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">api.ferstar.org</span> <span class="s">api</span><span class="p">;</span>   <span class="c1"># 微信机器人 -> 转发至本地 8444
</span></span></span><span class="line"><span class="cl">        <span class="kn">fm.ferstar.org</span>  <span class="s">fm</span><span class="p">;</span>    <span class="c1"># 文件服务器 -> 转发至本地 8445
</span></span></span><span class="line"><span class="cl">        <span class="kn">default</span>         <span class="s">ssh</span><span class="p">;</span>   <span class="c1"># 非 SSL 或未知域名默认转发至本地 22 (SSH)
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">ssh</span>  <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">22</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">api</span>  <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">8444</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">fm</span>   <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">8445</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="kn">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">listen</span> <span class="mi">443</span> <span class="s">reuseport</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_pass</span> <span class="nv">$stream_map</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_preread</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">3.2 架构优势
    <div id="32-架构优势" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#32-%e6%9e%b6%e6%9e%84%e4%bc%98%e5%8a%bf" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li><strong>极简防火墙</strong>: 对外只需暴露一个 443 端口即可承载多种协议（HTTP/SSH/Proxy）。</li>
<li><strong>安全性</strong>: 隐藏了 SSH (22) 等敏感端口，有效对抗暴力破解扫描。</li>
<li><strong>协议共存</strong>: 真正的协议分流，不影响标准 HTTPS 访问，绕过严苛网络环境。</li>
</ul>

<h3 class="relative group">3.3 异步 IO 优化
    <div id="33-异步-io-优化" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#33-%e5%bc%82%e6%ad%a5-io-%e4%bc%98%e5%8c%96" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>在 <code>http</code> 块中启用线程池异步 IO，避免大文件读写阻塞主进程：</p>
<ul>
<li><code>aio threads;</code></li>
<li><code>thread_pool default threads=32 max_queue=65536;</code></li>
<li><code>directio 4m;</code></li>
</ul>

<h2 class="relative group">4. 防火墙配置 (UFW)
    <div id="4-防火墙配置-ufw" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-%e9%98%b2%e7%81%ab%e5%a2%99%e9%85%8d%e7%bd%ae-ufw" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>实施最小化端口开放策略，关闭 22 端口入站（由 443 转发替代）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ufw reset
</span></span><span class="line"><span class="cl">ufw default deny incoming
</span></span><span class="line"><span class="cl">ufw default allow outgoing
</span></span><span class="line"><span class="cl">ufw allow 80/tcp
</span></span><span class="line"><span class="cl">ufw allow 443/tcp
</span></span><span class="line"><span class="cl">ufw allow 443/udp
</span></span><span class="line"><span class="cl">ufw allow 18443:18445/udp  <span class="c1"># 供代理服务使用</span>
</span></span><span class="line"><span class="cl">ufw enable</span></span></code></pre></div></div>

<h2 class="relative group">5. Let’s Encrypt 泛域名证书管理
    <div id="5-lets-encrypt-泛域名证书管理" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-lets-encrypt-%e6%b3%9b%e5%9f%9f%e5%90%8d%e8%af%81%e4%b9%a6%e7%ae%a1%e7%90%86" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">5.1 DNS 验证配置 (Cloudflare)
    <div id="51-dns-验证配置-cloudflare" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#51-dns-%e9%aa%8c%e8%af%81%e9%85%8d%e7%bd%ae-cloudflare" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>泛域名证书 (<code>*.ferstar.org</code>) 采用 <code>dns-cloudflare</code> 插件自动续期。</p>
<ul>
<li><strong>凭证文件</strong>: <code>/root/certbot-creds.ini</code> (包含 CF API Token)。</li>
<li><strong>插件安装</strong>: <code>apt install python3-certbot-dns-cloudflare -y</code>。</li>
</ul>

<h3 class="relative group">5.2 自动续期逻辑
    <div id="52-自动续期逻辑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#52-%e8%87%aa%e5%8a%a8%e7%bb%ad%e6%9c%9f%e9%80%bb%e8%be%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>续期配置位于 <code>/etc/letsencrypt/renewal/ferstar.org.conf</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">post_hook</span> <span class="o">=</span> systemctl reload nginx <span class="o">&&</span> docker restart hysteria hysteria2 tuic-server</span></span></code></pre></div></div>

<h2 class="relative group">6. 应用配置模板
    <div id="6-应用配置模板" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#6-%e5%ba%94%e7%94%a8%e9%85%8d%e7%bd%ae%e6%a8%a1%e6%9d%bf" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">6.1 Hysteria v1
    <div id="61-hysteria-v1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#61-hysteria-v1" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hysteria</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">tobyxdd/hysteria:v1.3.5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hysteria</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-config"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/etc/config.json"</span><span class="p">,</span><span class="w"> </span><span class="s2">"server"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.json:/etc/config.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18443:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sysctls</span><span class="p">:</span><span class="w"> </span>{<span class="nt">net.ipv4.tcp_congestion_control</span><span class="p">:</span><span class="w"> </span><span class="l">bbr}</span></span></span></code></pre></div></div>
<p><strong><code>config.json</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"listen"</span><span class="p">:</span> <span class="s2">":443"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"cert"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/fullchain.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"key"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/privkey.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"auth"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"mode"</span><span class="p">:</span> <span class="s2">"passwords"</span><span class="p">,</span> <span class="nt">"config"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"PASSWORD"</span><span class="p">]</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"up_mbps"</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"down_mbps"</span><span class="p">:</span> <span class="mi">1000</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">6.2 Hysteria v2
    <div id="62-hysteria-v2" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#62-hysteria-v2" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hysteria2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">tobyxdd/hysteria:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hysteria2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"server"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-c"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/etc/hysteria/config.yaml"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yaml:/etc/hysteria/config.yaml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18445:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sysctls</span><span class="p">:</span><span class="w"> </span>{<span class="nt">net.ipv4.tcp_congestion_control</span><span class="p">:</span><span class="w"> </span><span class="l">bbr}</span></span></span></code></pre></div></div>
<p><strong><code>config.yaml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">listen</span><span class="p">:</span><span class="w"> </span><span class="p">:</span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cert</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/letsencrypt/live/DOMAIN/fullchain.pem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/letsencrypt/live/DOMAIN/privkey.pem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s2">"PASSWORD"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bandwidth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">up</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="l">gbps</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">down</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="l">gbps</span></span></span></code></pre></div></div>

<h3 class="relative group">6.3 TUIC v5
    <div id="63-tuic-v5" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#63-tuic-v5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tuic</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/itsusinn/tuic-server:1.4.5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">tuic-server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18444:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.json:/etc/tuic/config.json:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span></span></span></code></pre></div></div>
<p><strong><code>config.json</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"server"</span><span class="p">:</span> <span class="s2">"[::]:443"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"users"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"UUID"</span><span class="p">:</span> <span class="s2">"PASSWORD"</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"certificate"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/fullchain.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"private_key"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/privkey.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"alpn"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"h3"</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"udp_relay_ipv6"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"zero_rtt_handshake"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"dual_stack"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"log_level"</span><span class="p">:</span> <span class="s2">"warn"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">6.4 Filebrowser
    <div id="64-filebrowser" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#64-filebrowser" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">filebrowser</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">filebrowser/filebrowser:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">filebrowser</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="p">:</span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"127.0.0.1:1122:80"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/root/fm/filebrowser/srv:/srv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/root/fm/filebrowser/database.db:/database.db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"--address"</span><span class="p">,</span><span class="w"> </span><span class="s2">"0.0.0.0"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--port"</span><span class="p">,</span><span class="w"> </span><span class="s2">"80"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--database"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/database.db"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--root"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/srv"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>AI-First 博客治理记录：SEO、重定向与中英双语落地</title>
      <link>https://blog.ferstar.org/posts/blog-seo-multilingual-ai-optimization/</link>
      <pubDate>Tue, 06 Jan 2026 03:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/blog-seo-multilingual-ai-optimization/</guid>
      <description>一次把博客的重定向、元描述和双语架构收拾干净的记录：清理 300+ legacy redirects，基于 GSC 调整标题/描述，并补齐高保真中英双语。</description><content:encoded><![CDATA[<p>这篇更像我的“修房记录”。几小时前，这个站点还只有中文：<code>static/_redirects</code> 里堆着多次迁移留下的规则，热门文章也缺少 description。</p>
<p>我用 Google Search Console (GSC) 先找症结，再动重定向，最后把双语架构和写作规范补齐。AI 在这轮里主要负责“批量脏活”和对照校验，方向和取舍还是我来拍板。</p>
<hr>

<h3 class="relative group">1. GSC 给我的一记闷棍：海外曝光很高，但没人点
    <div id="1-gsc-给我的一记闷棍海外曝光很高但没人点" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-gsc-%e7%bb%99%e6%88%91%e7%9a%84%e4%b8%80%e8%ae%b0%e9%97%b7%e6%a3%8d%e6%b5%b7%e5%a4%96%e6%9b%9d%e5%85%89%e5%be%88%e9%ab%98%e4%bd%86%e6%b2%a1%e4%ba%ba%e7%82%b9" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li><strong>美国市场</strong>：展示量 13,000+，CTR 只有 <strong>1.46%</strong></li>
<li><strong>华语市场</strong>：CTR 稳定在 6%+</li>
</ul>
<p>我原本以为“硬核内容会自己传播”，但现实更直接：全中文的页面，对很多海外读者来说就是一道门槛。做双语不是为了显得国际化，而是让搜到的人能读下去。</p>
<hr>

<h3 class="relative group">2. 重定向：最不起眼，但最容易把收录搞崩
    <div id="2-重定向最不起眼但最容易把收录搞崩" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e9%87%8d%e5%ae%9a%e5%90%91%e6%9c%80%e4%b8%8d%e8%b5%b7%e7%9c%bc%e4%bd%86%e6%9c%80%e5%ae%b9%e6%98%93%e6%8a%8a%e6%94%b6%e5%bd%95%e6%90%9e%e5%b4%a9" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Farbox -> Bitcron -> Hugo 迁了几次之后，<code>static/_redirects</code> 变得又长又乱。我一开始还想偷懒：把空格换成 <code>-</code>，再把多余的 <code>-</code> 合并掉，应该就差不多了。</p>
<p>然后就踩坑了：Hugo 处理带 <code>&</code> 的文件名时，会生成 <code>google-search-tips--tricks</code> 这种 <code>--</code> 的 slug。如果脚本把 <code>--</code> 合并成 <code>-</code>，旧的 Google 结果会直接 404。</p>
<p>最后我的做法是：</p>
<ul>
<li>同时兼容“单横杠标准版”和“多横杠原始版”（<code>--</code>、<code>---</code> 等），都精准指向新 slug</li>
<li>把模糊的通配符（比如 <code>/post/*</code>）替换成显式映射，方便后续维护</li>
</ul>
<p>这轮一口气把 300+ 条规则整理到一个我敢长期用的状态。</p>
<hr>

<h3 class="relative group">3. 双语：我不想要“只有英文摘要”的那种
    <div id="3-双语我不想要只有英文摘要的那种" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e5%8f%8c%e8%af%ad%e6%88%91%e4%b8%8d%e6%83%b3%e8%a6%81%e5%8f%aa%e6%9c%89%e8%8b%b1%e6%96%87%e6%91%98%e8%a6%81%e7%9a%84%e9%82%a3%e7%a7%8d" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>翻译我只设三条硬要求（不满足就不发）：</p>
<ol>
<li><strong>代码对齐</strong>：Shell 指令、Java Hook、内核配置逐字符一致</li>
<li><strong>图表保留</strong>：Mermaid 图在英文页面也能正常渲染</li>
<li><strong>细节留存</strong>：原文里的坑、取舍、性能数据别被翻成“白开水”</li>
</ol>
<p>流程上，AI 负责初稿和对照检查，我自己逐段改到读起来顺。首批先把 10+ 篇高流量文章补齐英文。</p>
<hr>

<h3 class="relative group">4. 把规则写下来：AGENTS.md
    <div id="4-把规则写下来agentsmd" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-%e6%8a%8a%e8%a7%84%e5%88%99%e5%86%99%e4%b8%8b%e6%9d%a5agentsmd" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>做完这轮我最怕的是“过几个月又忘了自己改过什么”。所以我在仓库里加了 <code>AGENTS.md</code>，把几条底线写死：</p>
<ul>
<li><strong>双语对称</strong>：中文更新，英文必须同步高保真跟进</li>
<li><strong>SEO 公式</strong>：description 按 <code>[痛点] + [方案] + [收益]</code> 写</li>
<li><strong>目录约定</strong>：<code>content.en/</code> 的结构和链接规则怎么处理</li>
</ul>
<p>这样下次不管是我自己回来看，还是让 Agent 继续干活，都能按同一套规矩来。</p>
<hr>

<h3 class="relative group">结语
    <div id="结语" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bb%93%e8%af%ad" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>我写博客是为了把“怎么解决问题”留下来；SEO 和双语只是让这些内容更容易被找到、更容易被读完。AI 对我来说更像加速器：它能帮我把脏活做快，但最后的判断和责任，还是得我自己扛。</p>
<hr>
<p><em>本文由 ferstar 整理，AI 协助对照与翻译。</em></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>基于 Teleport &#43; Tailscale 的 GPU 集群访问控制与审计实战</title>
      <link>https://blog.ferstar.org/posts/teleport-tailscale-gpu-access/</link>
      <pubDate>Sat, 03 Jan 2026 03:58:20 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/teleport-tailscale-gpu-access/</guid>
      <description>供应商需远程接入但审计必须静默；用 Teleport + Tailscale 的复用与绕过通道方案；实现安全且低摩擦的 GPU 集群访问。</description><content:encoded><![CDATA[<p>有几台 DGX H800 在内网，偶尔需要供应商远程调试。目标很明确：</p>
<ol>
<li>供应商能连上机器干活</li>
<li>全程录屏审计，但别让他知道在录（“静默”）</li>
<li>我们自己人要有特权通道，绕过堡垒机直连</li>
</ol>
<p>最终方案：<strong>Teleport</strong> 做堡垒机与审计，<strong>Tailscale</strong> 负责内网互通与运维绕行通道。下面是踩坑记录和关键配置。</p>
<hr>

<h3 class="relative group">架构速览
    <div id="架构速览" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9e%b6%e6%9e%84%e9%80%9f%e8%a7%88" aria-label="锚点">#</a>
    </span>
    
</h3>
<pre class="not-prose mermaid">flowchart LR
  subgraph External[公网用户]
    Vendor[供应商<br/>Web/SSH]
    AppUser[应用访问者<br/>Web]
  end
  subgraph Ops[内部运维]
    Admin[管理员<br/>SSH 直连]
  end
  subgraph Edge[公网接入层]
    Nginx[Nginx 443]
    Teleport[Teleport Proxy 3080]
    FW[DOCKER-USER 防火墙]
    Logs[(会话录屏/日志)]
    ExitNode[Tailscale Exit Node]
  end
  subgraph Intranet[内网 GPU 集群]
    GPU[GPU 节点]
    AppUI[App UI 32000]
  end

  Vendor -- HTTPS/SSH --> Nginx --> FW --> Teleport --> GPU
  AppUser -- HTTPS --> Nginx --> FW --> Teleport --> AppUI
  Admin -- SSH 22 --> TSUser[内部 Tailscale 节点]
  TSUser -- Tailscale 隧道 --> ExitNode --> GPU
  Teleport --> Logs</pre>
<hr>

<h3 class="relative group">踩坑 1：端口太多
    <div id="踩坑-1端口太多" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91-1%e7%ab%af%e5%8f%a3%e5%a4%aa%e5%a4%9a" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Teleport 默认要开 3022-3080 多个端口，防火墙规则配置很麻烦。启用 <strong>Multiplexing</strong> 后可单端口搞定：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># teleport.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auth_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy_listener_mode</span><span class="p">:</span><span class="w"> </span><span class="l">multiplex</span></span></span></code></pre></div></div>
<p>原理是 ALPN 自动识别流量类型，HTTPS/SSH/Tunnel 都走 3080。防火墙只管一个口就够了。</p>
<hr>

<h3 class="relative group">踩坑 2：内网节点装 Agent 装不上
    <div id="踩坑-2内网节点装-agent-装不上" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91-2%e5%86%85%e7%bd%91%e8%8a%82%e7%82%b9%e8%a3%85-agent-%e8%a3%85%e4%b8%8d%e4%b8%8a" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>GPU 节点在隔离内网，装 Teleport Agent 需要走代理。yum/dnf 报证书错误，最后发现要在 repo 里显式配代理：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/yum.repos.d/teleport.repo</span>
</span></span><span class="line"><span class="cl"><span class="k">[teleport]</span>
</span></span><span class="line"><span class="cl"><span class="na">proxy</span><span class="o">=</span><span class="s">http://[堡垒机VIP]:8888</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">踩坑 3：WebSocket 断连
    <div id="踩坑-3websocket-断连" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91-3websocket-%e6%96%ad%e8%bf%9e" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Nginx 反代 Teleport Web Terminal，页面能打开但终端立刻断开。查了半天，三个配置缺一不可：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">proxy_pass</span> <span class="s">https://127.0.0.1:3080</span><span class="p">;</span>  <span class="c1"># 必须 https
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_ssl_verify</span> <span class="no">off</span><span class="p">;</span>                <span class="c1"># 自签名证书
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Upgrade</span> <span class="nv">$http_upgrade</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Connection</span> <span class="s">"upgrade"</span><span class="p">;</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">踩坑 4：Docker 端口绕过 iptables
    <div id="踩坑-4docker-端口绕过-iptables" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91-4docker-%e7%ab%af%e5%8f%a3%e7%bb%95%e8%bf%87-iptables" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这个最坑。Docker 映射的端口会绕过 <code>INPUT</code> 链，直接走 <code>DOCKER-USER</code>。之前写的规则形同虚设，3080 对公网敞开着。</p>
<p>注意：<code>-F</code> 会清空 <code>DOCKER-USER</code> 规则，生产环境请先备份，或改用 <code>-I</code> 插入。</p>
<p>修复版：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iptables -F DOCKER-USER
</span></span><span class="line"><span class="cl"><span class="c1"># 只允许本地和 Tailscale 网段</span>
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -s 127.0.0.1 -p tcp --dport <span class="m">3080</span> -j ACCEPT
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -s 100.64.0.0/10 -p tcp --dport <span class="m">3080</span> -j ACCEPT
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -p tcp --dport <span class="m">3080</span> -j DROP</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">踩坑 5：Nginx 指向 VPN IP 导致循环依赖
    <div id="踩坑-5nginx-指向-vpn-ip-导致循环依赖" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91-5nginx-%e6%8c%87%e5%90%91-vpn-ip-%e5%af%bc%e8%87%b4%e5%be%aa%e7%8e%af%e4%be%9d%e8%b5%96" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>早期 Nginx 配的是 Tailscale VPN IP，结果 VPN 一抖，管理页不可用。</p>
<p>改成 <code>127.0.0.1</code> 本地回环，VPN 挂了照样能从公网进管理台。</p>
<hr>

<h3 class="relative group">权限设计
    <div id="权限设计" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9d%83%e9%99%90%e8%ae%be%e8%ae%a1" aria-label="锚点">#</a>
    </span>
    
</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">角色</th>
          <th style="text-align: left">登录方式</th>
          <th style="text-align: left">审计</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">管理员</td>
          <td style="text-align: left">SSH 22 端口 + Ed25519 密钥</td>
          <td style="text-align: left">无</td>
      </tr>
      <tr>
          <td style="text-align: left">供应商</td>
          <td style="text-align: left">Web Terminal</td>
          <td style="text-align: left">全程录屏，自己看不到录像</td>
      </tr>
  </tbody>
</table>
<p>RBAC 配置要点：给供应商的 <code>restricted-dev</code> 角色剥离 <code>audit</code> 权限，实现"静默审计"。
注意：剥离 <code>audit</code> 仅影响录像/日志的可见性，不一定隐藏录制提示，具体以 Teleport 版本与配置为准。</p>
<p>补充两条更实用的限制项：</p>
<ol>
<li>供应商仅通过 Teleport Web/SSH 访问，不需要装 Tailscale。</li>
<li>供应商角色限制登录账号与可见节点（用标签隔离）。</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># 示例：节点打标（供应商名称已脱敏）</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vendor</span><span class="p">:</span><span class="w"> </span><span class="l">vendor-x</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="l">prod</span></span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># 示例：角色限制（供应商账号与可见节点）</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">allow</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logins</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"vendor-user"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">node_labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">vendor</span><span class="p">:</span><span class="w"> </span><span class="s2">"vendor-x"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deny</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logins</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"root"</span><span class="p">]</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">核心配置
    <div id="核心配置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%a0%b8%e5%bf%83%e9%85%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong>docker-compose.yml</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">teleport</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'3080:3080'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'127.0.0.1:3025:3025'</span><span class="w">  </span><span class="c"># 管理 API 只本地</span></span></span></code></pre></div></div>
<p><strong>teleport.yaml</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">proxy_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">public_addr</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">teleport.example.com:443, 100.64.0.x:3080]</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">App Access（补充）
    <div id="app-access补充" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#app-access%e8%a1%a5%e5%85%85" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong>应用暴露示例</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">app_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="s2">"yes"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app-ui</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="l">http://10.120.0.0:32000/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">public_addr</span><span class="p">:</span><span class="w"> </span><span class="l">app-ui.example.com</span></span></span></code></pre></div></div>
<p><strong>代理环境冲突（503）</strong></p>
<p>节点有 <code>HTTP_PROXY</code> 时，Teleport 可能错误走代理访问内网服务，导致 503。给 Systemd 加 <code>NO_PROXY</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/teleport.service</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,100.64.0.0/10"</span></span></span></code></pre></div></div>
<p><strong>Nginx 透传 Host 头</strong></p>
<p>多子域访问时必须透传 Host：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span></span></span></code></pre></div></div>
<hr>
<p>这套下来，供应商走 Web 干活，内部通过 Tailscale 直连。录像审计随时调取，公网侧不暴露集群。</p>
<p>挺好。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T03</span><span class="o">:</span><span class="mi">58</span><span class="o">:</span><span class="mi">20</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T05</span><span class="o">:</span><span class="mi">55</span><span class="o">:</span><span class="mi">29</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/95
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Vibe Coding 实战：当代码不再需要手写</title>
      <link>https://blog.ferstar.org/posts/vibe-coding-engineering-practice/</link>
      <pubDate>Wed, 31 Dec 2025 21:46:31 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/vibe-coding-engineering-practice/</guid>
      <description>AI 生成代码可控性差、验收成本高，通过工程化上下文与验证体系，让 AI 输出可用代码并稳定交付。</description><content:encoded><![CDATA[<blockquote><p>基于 Vibe Coding Skeleton 的工程实践
<strong>对齐说明</strong>：涉及到具体落地细节时，以本仓库 <code>CLAUDE.md</code> / <code>.pre-commit-config.yaml</code> / <code>ast-grep/</code> / <code>sgconfig.yml</code> / <code>justfile</code> 为准。</p>
</blockquote><hr>
<p><strong>TL;DR</strong>：不是“让 AI 写代码”，而是“用工程化让 AI 写对代码”。</p>
<hr>

<h3 class="relative group">一、一个事实
    <div id="一一个事实" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e4%b8%80%e4%b8%aa%e4%ba%8b%e5%ae%9e" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">1.1 数据冲击
    <div id="11-数据冲击" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#11-%e6%95%b0%e6%8d%ae%e5%86%b2%e5%87%bb" aria-label="锚点">#</a>
    </span>
    
</h4>
<blockquote><p>某个真实项目（已脱敏），持续迭代约 8 个月，累计 1k+ 次提交。</p>
<p>除了需求拆解、业务梳理、架构调整与最终验收——
<strong>绝大多数代码由 AI 生成，人负责决策与验收。</strong></p>
<p>今天不聊 AI 能不能写代码。
聊的是：<strong>怎么让 AI 写对代码。</strong></p>
</blockquote><p><strong>项目概况</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">指标</th>
          <th style="text-align: center">数值（脱敏）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">开发周期</td>
          <td style="text-align: center">约 8 个月</td>
      </tr>
      <tr>
          <td style="text-align: left">总提交</td>
          <td style="text-align: center">1k+ commits</td>
      </tr>
      <tr>
          <td style="text-align: left">活跃天数</td>
          <td style="text-align: center">200+ 天</td>
      </tr>
      <tr>
          <td style="text-align: left">活跃日均</td>
          <td style="text-align: center">~5 commits</td>
      </tr>
      <tr>
          <td style="text-align: left">Conventional Commits 规范率</td>
          <td style="text-align: center">>95%</td>
      </tr>
  </tbody>
</table>
<p><strong>提交类型分布</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">类型</th>
          <th style="text-align: center">占比</th>
          <th style="text-align: left">说明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">feat</td>
          <td style="text-align: center">≈40%</td>
          <td style="text-align: left">新功能</td>
      </tr>
      <tr>
          <td style="text-align: left">fix</td>
          <td style="text-align: center">≈20%</td>
          <td style="text-align: left">Bug 修复</td>
      </tr>
      <tr>
          <td style="text-align: left">refactor</td>
          <td style="text-align: center">≈20%</td>
          <td style="text-align: left">重构（含 AI 代码优化）</td>
      </tr>
      <tr>
          <td style="text-align: left">test</td>
          <td style="text-align: center">≈5%</td>
          <td style="text-align: left">测试</td>
      </tr>
      <tr>
          <td style="text-align: left">其他</td>
          <td style="text-align: center">其余</td>
          <td style="text-align: left">chore/docs/perf 等</td>
      </tr>
  </tbody>
</table>
<p><strong>开发阶段演进（抽象）</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">阶段</th>
          <th style="text-align: left">特征</th>
          <th style="text-align: left">重点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">功能冲刺期</td>
          <td style="text-align: left"><code>feat</code> 占比更高</td>
          <td style="text-align: left">快速验证主链路 + 质量门禁</td>
      </tr>
      <tr>
          <td style="text-align: left">稳定迭代期</td>
          <td style="text-align: left"><code>fix/refactor</code> 上升</td>
          <td style="text-align: left">自动化回归 + 边界/性能验证</td>
      </tr>
  </tbody>
</table>

<h4 class="relative group">1.2 角色转变
    <div id="12-角色转变" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#12-%e8%a7%92%e8%89%b2%e8%bd%ac%e5%8f%98" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>传统认知</strong> vs <strong>实际情况</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">传统认知</th>
          <th style="text-align: left">实际情况</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">AI 帮你写一部分代码</td>
          <td style="text-align: left">AI 生成大部分代码，人负责决策与验收</td>
      </tr>
      <tr>
          <td style="text-align: left">人 + AI 协作</td>
          <td style="text-align: left">人指挥，AI 执行，人验收</td>
      </tr>
      <tr>
          <td style="text-align: left">学会用 AI</td>
          <td style="text-align: left">学会验 AI、学会喂上下文</td>
      </tr>
  </tbody>
</table>
<p><strong>新的分工模式</strong>：</p>
<pre class="not-prose mermaid">flowchart TB
    subgraph 人的工作
        A[需求拆解] --> B[业务梳理]
        B --> C[架构决策]
        C --> D[上下文准备]
        D --> E[验证审查]
    end

    subgraph AI的工作
        F[Controller]
        G[Service]
        H[Schema]
        I[Test]
        J[Migration]
        K[Config]
        L[Commit Message]
    end

    D --> F & G & H & I & J & K & L
    F & G & H & I & J & K & L --> E</pre>
<p><strong>角色定义</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">角色</th>
          <th style="text-align: left">职责</th>
          <th style="text-align: center">不可替代性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">产品经理</td>
          <td style="text-align: left">需求拆解、优先级</td>
          <td style="text-align: center">⭐⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left">架构师</td>
          <td style="text-align: left">技术选型、模块划分</td>
          <td style="text-align: center">⭐⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left">QA</td>
          <td style="text-align: left">验证、边界条件</td>
          <td style="text-align: center">⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left"><del>Coder</del></td>
          <td style="text-align: left"><del>写代码</del></td>
          <td style="text-align: center"><del>AI 替代</del></td>
      </tr>
  </tbody>
</table>
<hr>

<h3 class="relative group">二、为什么能做到
    <div id="二为什么能做到" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%ba%8c%e4%b8%ba%e4%bb%80%e4%b9%88%e8%83%bd%e5%81%9a%e5%88%b0" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">2.1 上下文为王
    <div id="21-上下文为王" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#21-%e4%b8%8a%e4%b8%8b%e6%96%87%e4%b8%ba%e7%8e%8b" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>核心公式</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">AI 写代码的水平 = 你提供上下文的水平</span></span></code></pre></div></div>
<p><strong>更完整的版本</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">AI 交付质量 = 上下文质量 × 验证自动化 × 任务适配度</span></span></code></pre></div></div>
<p>没有验证，输出只能算草稿；任务不适配，验证成本会爆炸。</p>
<p><strong>上下文三层模型</strong>：</p>
<pre class="not-prose mermaid">graph TB
    subgraph 上下文金字塔
        P["Project Context<br/>技术栈 / 目录结构 / 命名规范 / 禁令<br/>CLAUDE.md"]
        T["Task Context<br/>需求 / 边界 / 验收标准 / 领域术语<br/>Prompt"]
        S["Session Context<br/>对话历史 / 中间结果<br/>自动累积"]
    end

    P --> T --> S

    style P fill:#4ecdc4,stroke:#333,stroke-width:2px
    style T fill:#f38181
    style S fill:#fce38a</pre>
<p><strong>各层级详解</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">层级</th>
          <th style="text-align: left">核心问题</th>
          <th style="text-align: left">具体内容</th>
          <th style="text-align: left">载体</th>
          <th style="text-align: left">投入</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Project</strong></td>
          <td style="text-align: left">这个项目怎么玩？</td>
          <td style="text-align: left">技术栈、目录约定、命名规范、零容忍规则、常用命令</td>
          <td style="text-align: left">CLAUDE.md</td>
          <td style="text-align: left">一次性</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Task</strong></td>
          <td style="text-align: left">这次要做什么？</td>
          <td style="text-align: left">功能需求、边界条件、不要做什么、相关领域术语</td>
          <td style="text-align: left">Prompt</td>
          <td style="text-align: left">每任务</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Session</strong></td>
          <td style="text-align: left">刚才聊了什么？</td>
          <td style="text-align: left">会话历史、AI 的中间输出、已确认的决策</td>
          <td style="text-align: left">自动</td>
          <td style="text-align: left">无</td>
      </tr>
  </tbody>
</table>
<p><strong>投资回报</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">投入产出比：Project >> Task >> Session
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Project：8h 投入 → 所有任务受益（复利）
</span></span><span class="line"><span class="cl">Task：5-10min → 当次任务
</span></span><span class="line"><span class="cl">Session：0 → 当次对话</span></span></code></pre></div></div>
<p><strong>结论</strong>：Project Context 是"一次投入，持续受益"，复利最高。把精力花在写好 CLAUDE.md 上，是 ROI 最高的投资。</p>

<h4 class="relative group">2.2 瓶颈转移
    <div id="22-瓶颈转移" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#22-%e7%93%b6%e9%a2%88%e8%bd%ac%e7%a7%bb" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>开发流程对比</strong>：</p>
<pre class="not-prose mermaid">graph TB
    subgraph 传统开发
        A1[需求] --> A2[设计] --> A3[编码] --> A4[测试] --> A5[部署]
    end
    subgraph AI辅助开发
        B1[需求] --> B2[设计] --> B3[AI生成] --> B4[验证] --> B5[部署]
    end

    style A3 fill:#ff6b6b,stroke:#333,stroke-width:3px
    style B4 fill:#ff6b6b,stroke:#333,stroke-width:3px</pre>
<p><strong>我的时间分配</strong>：</p>
<pre class="not-prose mermaid">pie title 时间分配（经验值）
    "需求理解/架构设计" : 20
    "准备上下文" : 15
    "AI 生成(等待)" : 5
    "验证 + 修复" : 40
    "技术债偿还" : 20</pre>
<p><strong>关键洞察</strong>：</p>
<ul>
<li>40% 时间在「验」，不是「写」</li>
<li>验证/修复相关的投入往往不低于“写新功能”</li>
<li>这印证了瓶颈从「写」转到「验」</li>
</ul>
<hr>

<h3 class="relative group">三、怎么做到的
    <div id="三怎么做到的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%89%e6%80%8e%e4%b9%88%e5%81%9a%e5%88%b0%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">3.1 项目级上下文：CLAUDE.md
    <div id="31-项目级上下文claudemd" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#31-%e9%a1%b9%e7%9b%ae%e7%ba%a7%e4%b8%8a%e4%b8%8b%e6%96%87claudemd" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>本项目用一个约 300 行的 <code>CLAUDE.md</code> 作为「AI 入职手册」。</p>
<blockquote><p><strong>注</strong>：不同 AI 工具对“上下文文件”的文件名有偏好（例如 <code>CLAUDE.md</code>、<code>.cursorrules</code>、<code>AGENTS.md</code>）。本质都是同一件事：把项目规则写成机器可读的入职手册。</p>
</blockquote><p><strong>核心结构</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># CLAUDE.md
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Project Overview        # 项目是干什么的
</span></span></span><span class="line"><span class="cl"><span class="gu">## Technology Stack        # 技术栈：Litestar + PostgreSQL + Redis + OpenDAL + Celery
</span></span></span><span class="line"><span class="cl"><span class="gu">## Basic Rules             # 基本规则：先激活 venv，用 uv 管理依赖
</span></span></span><span class="line"><span class="cl"><span class="gu">## Development Commands    # 常用命令：just lint, just test
</span></span></span><span class="line"><span class="cl"><span class="gu">## Architecture Overview   # 架构：DDD 分层，src/domain/ 下按业务组织
</span></span></span><span class="line"><span class="cl"><span class="gu">## Coding Style            # 风格：ruff 格式化，150 字符行宽
</span></span></span><span class="line"><span class="cl"><span class="gu">## Testing Guidelines      # 测试：pytest（fail-fast，warnings-as-errors）
</span></span></span><span class="line"><span class="cl">## CLI Commands            # CLI：litestar database upgrade, litestar run</span></span></code></pre></div></div>
<p><strong>为什么有效</strong>：AI 不用猜，直接按规范来。</p>

<h5 class="relative group">案例：ast-grep 如何拦截 AI 的坏习惯
    <div id="案例ast-grep-如何拦截-ai-的坏习惯" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%a1%88%e4%be%8bast-grep-%e5%a6%82%e4%bd%95%e6%8b%a6%e6%88%aa-ai-%e7%9a%84%e5%9d%8f%e4%b9%a0%e6%83%af" aria-label="锚点">#</a>
    </span>
    
</h5>
<p><strong>需求</strong>：写一个处理 PDF 临时文件的函数</p>
<p><strong>AI 输出</strong>（常见模式）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">tempfile</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process_pdf</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 使用 delete=False 以便后续处理</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">suffix</span><span class="o">=</span><span class="s2">".pdf"</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span> <span class="k">as</span> <span class="n">tmp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">tmp</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">parse_pdf</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">missing_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>  <span class="c1"># 手动清理</span></span></span></code></pre></div></div>
<p><strong>问题</strong>：</p>
<ul>
<li><code>delete=False</code> 强迫引入手动清理逻辑，代码更长、维护成本更高</li>
<li>这类清理逻辑一旦遗漏/写错（例如忘记 <code>try/finally</code>），就会引入临时文件泄漏</li>
<li>同类代码越多，review 与一致性成本越高</li>
</ul>
<p><strong>ast-grep 拦截</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git commit -m <span class="s2">"feat: add pdf processor"</span>
</span></span><span class="line"><span class="cl">error<span class="o">[</span>no-tempfile-delete-false<span class="o">]</span>: NamedTemporaryFile with <span class="nv">delete</span><span class="o">=</span>False is forbidden
</span></span><span class="line"><span class="cl">  --> src/utils/pdf.py:6:10
</span></span><span class="line"><span class="cl">   <span class="p">|</span>
</span></span><span class="line"><span class="cl"> <span class="m">6</span> <span class="p">|</span>     with tempfile.NamedTemporaryFile<span class="o">(</span><span class="nv">suffix</span><span class="o">=</span><span class="s2">".pdf"</span>, <span class="nv">delete</span><span class="o">=</span>False<span class="o">)</span> as tmp:
</span></span><span class="line"><span class="cl">   <span class="p">|</span>          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></span></code></pre></div></div>
<p><strong>修复后</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process_pdf</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">suffix</span><span class="o">=</span><span class="s2">".pdf"</span><span class="p">)</span> <span class="k">as</span> <span class="n">tmp</span><span class="p">:</span>  <span class="c1"># 默认 delete=True</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">tmp</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>  <span class="c1"># 确保写入磁盘</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">parse_pdf</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 退出 with 块时自动清理，无泄漏风险</span></span></span></code></pre></div></div>
<p><strong>关键点</strong>：不是靠 AI “自觉遵守” CLAUDE.md，而是靠 ast-grep <strong>强制拦截</strong>。AI 可能还是会犯错，但错误代码无法通过 pre-commit 进入代码库。</p>

<h4 class="relative group">3.2 自动化验证体系
    <div id="32-自动化验证体系" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#32-%e8%87%aa%e5%8a%a8%e5%8c%96%e9%aa%8c%e8%af%81%e4%bd%93%e7%b3%bb" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>AI 生成的代码必须过质检流水线：门禁不过，就不应该进入主分支（至少先别推送/合入）。</p>
<p>本仓库示例把门禁分布在 <code>pre-commit</code> / <code>commit-msg</code> / <code>pre-push</code> 三个阶段。</p>
<p><strong>验证流水线</strong>：</p>
<pre class="not-prose mermaid">flowchart LR
    A[代码修改] --> B[pre-commit: ruff format]
    B --> C[pre-commit: ruff check]
    C --> D[pre-commit: ast-grep scan]
    D --> E[git commit]
    E --> F[commit-msg: Conventional Commits]
    F --> G[git push]
    G --> H[pre-push: pytest]

    style D fill:#ff6b6b,stroke:#333,stroke-width:2px
    style H fill:#4ecdc4,stroke:#333,stroke-width:2px</pre>
<p><strong>pre-commit 核心配置</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Hook（id）</th>
          <th style="text-align: left">作用</th>
          <th style="text-align: center">阶段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">ruff-format</td>
          <td style="text-align: left">代码格式化</td>
          <td style="text-align: center">pre-commit</td>
      </tr>
      <tr>
          <td style="text-align: left">ruff</td>
          <td style="text-align: left">静态检查 + 自动修复</td>
          <td style="text-align: center">pre-commit</td>
      </tr>
      <tr>
          <td style="text-align: left">ast-grep-lint</td>
          <td style="text-align: left">项目特定规则</td>
          <td style="text-align: center">pre-commit</td>
      </tr>
      <tr>
          <td style="text-align: left">conventional-pre-commit</td>
          <td style="text-align: left">提交信息规范（Conventional Commits）</td>
          <td style="text-align: center">commit-msg</td>
      </tr>
      <tr>
          <td style="text-align: left">pytest</td>
          <td style="text-align: left">单元测试</td>
          <td style="text-align: center">pre-push</td>
      </tr>
  </tbody>
</table>

<h5 class="relative group">ast-grep：项目特定的 AI 防护栏
    <div id="ast-grep项目特定的-ai-防护栏" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ast-grep%e9%a1%b9%e7%9b%ae%e7%89%b9%e5%ae%9a%e7%9a%84-ai-%e9%98%b2%e6%8a%a4%e6%a0%8f" aria-label="锚点">#</a>
    </span>
    
</h5>
<p>AI 不知道项目的特殊约束，但 ast-grep 会自动拦截。</p>
<p><strong>本仓库示例规则（节选）</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">规则（ast-grep id）</th>
          <th style="text-align: left">拦截什么</th>
          <th style="text-align: left">为什么</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">no-match-case</td>
          <td style="text-align: left"><code>match/case</code> 语法</td>
          <td style="text-align: left">Cython 兼容性</td>
      </tr>
      <tr>
          <td style="text-align: left">no-tempfile-delete-false</td>
          <td style="text-align: left"><code>NamedTemporaryFile(delete=False)</code></td>
          <td style="text-align: left">防止临时文件泄漏，减少手写清理逻辑</td>
      </tr>
      <tr>
          <td style="text-align: left">no-global-src-import-in-cli</td>
          <td style="text-align: left"><code>**/cli.py</code> 全局导入 <code>src.*</code></td>
          <td style="text-align: left">避免启动副作用/循环依赖，CLI 更轻</td>
      </tr>
      <tr>
          <td style="text-align: left">no-global-src-import-in-tests</td>
          <td style="text-align: left">测试文件全局导入 <code>src.*</code>（常量除外）</td>
          <td style="text-align: left">提升隔离性，减少副作用</td>
      </tr>
      <tr>
          <td style="text-align: left">no-local-import-in-production-code</td>
          <td style="text-align: left">生产代码里的“局部导入”</td>
          <td style="text-align: left">依赖可见，便于静态检查/审查</td>
      </tr>
      <tr>
          <td style="text-align: left">no-local-import-stdlib-third-party</td>
          <td style="text-align: left">函数内导入标准库/三方包</td>
          <td style="text-align: left">避免隐藏依赖与重复导入</td>
      </tr>
  </tbody>
</table>
<blockquote><p>规则定义在 <code>ast-grep/rules/</code>，由 <code>sgconfig.yml</code> 加载，并通过 <code>.pre-commit-config.yaml</code> 在提交前自动执行。</p>
</blockquote>
<h5 class="relative group">踩坑实录
    <div id="踩坑实录" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b8%a9%e5%9d%91%e5%ae%9e%e5%bd%95" aria-label="锚点">#</a>
    </span>
    
</h5>
<p><strong>案例：match/case 事件</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># AI 生成了 Python 3.10 的 match/case</span>
</span></span><span class="line"><span class="cl"><span class="k">match</span> <span class="n">value</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">"one"</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">_</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">"other"</span></span></span></code></pre></div></div>
<ul>
<li>Cython 编译失败</li>
<li>人工 review 没发现</li>
<li>教训：写了 ast-grep 规则永久拦截</li>
</ul>
<p><strong>实际效果</strong>：AI 生成了 <code>match/case</code>？提交时直接报错，不用等人工 review。</p>

<h5 class="relative group">ROI
    <div id="roi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#roi" aria-label="锚点">#</a>
    </span>
    
</h5>
<table>
  <thead>
      <tr>
          <th style="text-align: left">投入</th>
          <th style="text-align: left">回报</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">配置 pre-commit（2h）</td>
          <td style="text-align: left">每次提交自动检查</td>
      </tr>
      <tr>
          <td style="text-align: left">写 ast-grep 规则（4h）</td>
          <td style="text-align: left">防止 AI 犯特定错误</td>
      </tr>
      <tr>
          <td style="text-align: left">维护 CLAUDE.md（持续）</td>
          <td style="text-align: left">AI 输出质量提升</td>
      </tr>
  </tbody>
</table>
<p><strong>核心观点</strong>：自动化验证是信任 AI 的前提。</p>

<h4 class="relative group">3.3 标准化设计
    <div id="33-标准化设计" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#33-%e6%a0%87%e5%87%86%e5%8c%96%e8%ae%be%e8%ae%a1" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>标准化 = AI 友好</strong>。模式越固定，AI 越准确。</p>

<h5 class="relative group">DDD 分层
    <div id="ddd-分层" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ddd-%e5%88%86%e5%b1%82" aria-label="锚点">#</a>
    </span>
    
</h5>
<pre class="not-prose mermaid">graph TB
    subgraph "src/"
        subgraph "domain/ 业务领域"
            US[users]
            FI[files]
        end

        subgraph "每个领域（固定四件套）"
            C[controller.py<br/>HTTP 端点]
            S[service.py<br/>业务逻辑]
            SC[schema.py<br/>请求/响应 Schema]
            R[repository.py<br/>数据访问]
        end

        DB[db/<br/>models + session]
        UT[utils/<br/>工具函数]
    end

    US --> C & S & SC & R
    FI --> C & S & SC & R
    C --> S --> R --> DB</pre>

<h5 class="relative group">命名规范
    <div id="命名规范" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%91%bd%e5%90%8d%e8%a7%84%e8%8c%83" aria-label="锚点">#</a>
    </span>
    
</h5>
<table>
  <thead>
      <tr>
          <th style="text-align: left">类型</th>
          <th style="text-align: left">后缀</th>
          <th style="text-align: left">示例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Controller</td>
          <td style="text-align: left"><code>*Controller</code></td>
          <td style="text-align: left"><code>UserController</code> / <code>FileController</code></td>
      </tr>
      <tr>
          <td style="text-align: left">Service</td>
          <td style="text-align: left"><code>*Service</code></td>
          <td style="text-align: left"><code>UserService</code> / <code>FileService</code></td>
      </tr>
      <tr>
          <td style="text-align: left">Repository</td>
          <td style="text-align: left"><code>*Repository</code></td>
          <td style="text-align: left"><code>UserRepository</code> / <code>FileRepository</code></td>
      </tr>
      <tr>
          <td style="text-align: left">Schema</td>
          <td style="text-align: left"><code>*Request/*Response</code></td>
          <td style="text-align: left"><code>LoginRequest</code> / <code>UserResponse</code> / <code>FileResponse</code></td>
      </tr>
      <tr>
          <td style="text-align: left">模块</td>
          <td style="text-align: left">snake_case</td>
          <td style="text-align: left"><code>users</code> / <code>files</code></td>
      </tr>
      <tr>
          <td style="text-align: left">函数</td>
          <td style="text-align: left">snake_case</td>
          <td style="text-align: left"><code>get_user_by_id</code></td>
      </tr>
  </tbody>
</table>

<h5 class="relative group">目录约定（与本仓库一致）
    <div id="目录约定与本仓库一致" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%9b%ae%e5%bd%95%e7%ba%a6%e5%ae%9a%e4%b8%8e%e6%9c%ac%e4%bb%93%e5%ba%93%e4%b8%80%e8%87%b4" aria-label="锚点">#</a>
    </span>
    
</h5>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">src/domain/
</span></span><span class="line"><span class="cl">  users/
</span></span><span class="line"><span class="cl">    controller.py
</span></span><span class="line"><span class="cl">    service.py
</span></span><span class="line"><span class="cl">    repository.py
</span></span><span class="line"><span class="cl">    schema.py
</span></span><span class="line"><span class="cl">  files/
</span></span><span class="line"><span class="cl">    controller.py
</span></span><span class="line"><span class="cl">    service.py
</span></span><span class="line"><span class="cl">    repository.py
</span></span><span class="line"><span class="cl">    schema.py</span></span></code></pre></div></div>

<h4 class="relative group">3.4 提示词工程
    <div id="34-提示词工程" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#34-%e6%8f%90%e7%a4%ba%e8%af%8d%e5%b7%a5%e7%a8%8b" aria-label="锚点">#</a>
    </span>
    
</h4>

<h5 class="relative group">分步确认模式
    <div id="分步确认模式" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%88%86%e6%ad%a5%e7%a1%ae%e8%ae%a4%e6%a8%a1%e5%bc%8f" aria-label="锚点">#</a>
    </span>
    
</h5>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">我需要开发 [功能]。请按以下步骤：
</span></span><span class="line"><span class="cl">1. 先读相关代码了解架构
</span></span><span class="line"><span class="cl">2. 制定实现计划
</span></span><span class="line"><span class="cl">3. 实现核心功能
</span></span><span class="line"><span class="cl">4. 写测试
</span></span><span class="line"><span class="cl">每完成一步暂停等我确认。</span></span></code></pre></div></div>
<p><strong>为什么有效</strong>：复杂任务拆成小步，每步可回退，减少返工。</p>

<h5 class="relative group">常用技巧
    <div id="常用技巧" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e7%94%a8%e6%8a%80%e5%b7%a7" aria-label="锚点">#</a>
    </span>
    
</h5>
<table>
  <thead>
      <tr>
          <th style="text-align: left">技巧</th>
          <th style="text-align: left">说明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><code>@文件路径</code></td>
          <td style="text-align: left">精确引用文件，避免 AI 猜错</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>/clear</code></td>
          <td style="text-align: left">清理上下文，保持对话聚焦</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>/compact</code></td>
          <td style="text-align: left">压缩历史，释放 token</td>
      </tr>
      <tr>
          <td style="text-align: left">先要计划</td>
          <td style="text-align: left">先让 AI 输出实现计划/验收点，再进入编码</td>
      </tr>
  </tbody>
</table>
<blockquote><p>注：<code>/clear</code> / <code>/compact</code> 这类命令属于“工具能力”，不同客户端可能不同；核心目的是降噪、聚焦、降低返工。</p>
</blockquote>
<h5 class="relative group">核心原则
    <div id="核心原则" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%a0%b8%e5%bf%83%e5%8e%9f%e5%88%99" aria-label="锚点">#</a>
    </span>
    
</h5>
<pre class="not-prose mermaid">flowchart LR
    A[明确具体] --> B[设边界]
    B --> C[分步骤]
    C --> D[验证确认]

    A1["不说「优化一下」<br/>说「这里有 N+1 问题」"] -.-> A
    B1["明确什么要做<br/>什么不要做"] -.-> B
    C1["复杂任务拆开<br/>每步确认"] -.-> C
    D1["重要步骤<br/>要求确认再继续"] -.-> D</pre>
<hr>

<h3 class="relative group">四、边界与止损
    <div id="四边界与止损" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9b%9b%e8%be%b9%e7%95%8c%e4%b8%8e%e6%ad%a2%e6%8d%9f" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">4.1 甜点区 vs 泥潭
    <div id="41-甜点区-vs-泥潭" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#41-%e7%94%9c%e7%82%b9%e5%8c%ba-vs-%e6%b3%a5%e6%bd%ad" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>核心认知</strong>：不是「AI 能不能写」—— AI 都能写。是「验证成本高不高」。</p>
<pre class="not-prose mermaid">flowchart TB
    subgraph 验证成本高
        subgraph Q1["❌ 泥潭区"]
            Q1A["复杂业务"]
            Q1B["性能优化"]
            Q1C["算法设计"]
        end
    end
    subgraph 验证成本低
        subgraph Q3["✅ 甜点区"]
            Q3A["CRUD接口"]
            Q3B["单元测试"]
            Q3C["配置文件"]
            Q3D["数据库迁移"]
        end
    end

    style Q3 fill:#4ecdc4,stroke:#333
    style Q1 fill:#ff6b6b,stroke:#333</pre>
<p><strong>判断标准</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">任务</th>
          <th style="text-align: center">AI 能写？</th>
          <th style="text-align: center">验证成本</th>
          <th style="text-align: center">结论</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">CRUD 接口</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">低（跑测试）</td>
          <td style="text-align: center">✅ 甜点</td>
      </tr>
      <tr>
          <td style="text-align: left">单元测试</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">低（输入输出明确）</td>
          <td style="text-align: center">✅ 甜点</td>
      </tr>
      <tr>
          <td style="text-align: left">配置文件</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">低（格式固定）</td>
          <td style="text-align: center">✅ 甜点</td>
      </tr>
      <tr>
          <td style="text-align: left">数据库迁移</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">低（结构化）</td>
          <td style="text-align: center">✅ 甜点</td>
      </tr>
      <tr>
          <td style="text-align: left">复杂业务</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">高（需领域知识）</td>
          <td style="text-align: center">⚠️ 谨慎</td>
      </tr>
      <tr>
          <td style="text-align: left">性能优化</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">高（需实测）</td>
          <td style="text-align: center">❌ 泥潭</td>
      </tr>
      <tr>
          <td style="text-align: left">算法设计</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">高（需理解）</td>
          <td style="text-align: center">❌ 泥潭</td>
      </tr>
  </tbody>
</table>

<h5 class="relative group">案例：性能优化的正确姿势（脱敏）
    <div id="案例性能优化的正确姿势脱敏" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%a1%88%e4%be%8b%e6%80%a7%e8%83%bd%e4%bc%98%e5%8c%96%e7%9a%84%e6%ad%a3%e7%a1%ae%e5%a7%bf%e5%8a%bf%e8%84%b1%e6%95%8f" aria-label="锚点">#</a>
    </span>
    
</h5>
<p><strong>案例：N+1 查询优化</strong></p>
<p><strong>问题</strong>：测试环境某列表接口响应从百毫秒级飙到秒级。</p>
<p><strong>错误做法</strong> — 让 AI 在缺少数据的情况下“猜”：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">我：这个接口太慢了，帮我优化
</span></span><span class="line"><span class="cl">AI：加预加载 / 加缓存 / 加索引...（缺少证据，很可能无效）</span></span></code></pre></div></div>
<p><strong>正确做法</strong> — 先把“运行时事实”拿出来：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1. 人：在测试环境执行 EXPLAIN (ANALYZE, BUFFERS)，拿到查询计划
</span></span><span class="line"><span class="cl">2. AI（分析型）：根据计划定位瓶颈（例如某子查询导致重复扫描）
</span></span><span class="line"><span class="cl">3. 人：确认改写思路 + 需要的索引/迁移策略
</span></span><span class="line"><span class="cl">4. AI（执行型）：生成代码 + 迁移脚本 + 对应测试
</span></span><span class="line"><span class="cl">5. 人：跑 `just test` 并做简单压测对比</span></span></code></pre></div></div>
<p><strong>结果</strong>：通常能把“秒级”拉回到“百毫秒级”（具体数字因业务而异）。</p>
<p><strong>关键洞察</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">阶段</th>
          <th style="text-align: left">负责人</th>
          <th style="text-align: left">为什么</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">获取运行时证据</td>
          <td style="text-align: left">人</td>
          <td style="text-align: left">需要环境访问与判断“该怀疑什么”</td>
      </tr>
      <tr>
          <td style="text-align: left">分析与归因</td>
          <td style="text-align: left">AI</td>
          <td style="text-align: left">有数据就能更高效定位与总结</td>
      </tr>
      <tr>
          <td style="text-align: left">落地与验证</td>
          <td style="text-align: left">AI + 人</td>
          <td style="text-align: left">AI 写代码，人做最终验收</td>
      </tr>
  </tbody>
</table>
<p><strong>教训</strong>：不是“AI 不会优化”，而是“AI 不能替你拿到运行时数据”。</p>

<h4 class="relative group">4.2 放弃阈值
    <div id="42-放弃阈值" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#42-%e6%94%be%e5%bc%83%e9%98%88%e5%80%bc" aria-label="锚点">#</a>
    </span>
    
</h4>
<table>
  <thead>
      <tr>
          <th style="text-align: left">信号</th>
          <th style="text-align: left">行动</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">改 3 次还不对</td>
          <td style="text-align: left">自己写</td>
      </tr>
      <tr>
          <td style="text-align: left">解释 > 5 分钟领域知识</td>
          <td style="text-align: left">自己写</td>
      </tr>
      <tr>
          <td style="text-align: left">AI 反复犯同样错误</td>
          <td style="text-align: left">自己写</td>
      </tr>
      <tr>
          <td style="text-align: left">安全敏感逻辑</td>
          <td style="text-align: left">自己写 + 仔细审查</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：设止损线，别在泥潭里死磕。</p>

<h4 class="relative group">4.3 AI 常见错误（真实案例）
    <div id="43-ai-常见错误真实案例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#43-ai-%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%e7%9c%9f%e5%ae%9e%e6%a1%88%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h4>

<h5 class="relative group">1. 过度简化 — 破坏隐含逻辑
    <div id="1-过度简化--破坏隐含逻辑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e8%bf%87%e5%ba%a6%e7%ae%80%e5%8c%96--%e7%a0%b4%e5%9d%8f%e9%9a%90%e5%90%ab%e9%80%bb%e8%be%91" aria-label="锚点">#</a>
    </span>
    
</h5>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># AI "简化"前（正确）</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">server_session_id</span> <span class="o">!=</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]</span><span class="o">.</span><span class="n">encode</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># AI "简化"后（错误）</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">server_session_id</span> <span class="o">!=</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># bytes vs str 比较，永远不相等，所有用户被踢出登录</span></span></span></code></pre></div></div>
<p><strong>后果</strong>：用户认证失败，需回滚</p>

<h5 class="relative group">2. 遗漏关键调用 — 上下文不足
    <div id="2-遗漏关键调用--上下文不足" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e9%81%97%e6%bc%8f%e5%85%b3%e9%94%ae%e8%b0%83%e7%94%a8--%e4%b8%8a%e4%b8%8b%e6%96%87%e4%b8%8d%e8%b6%b3" aria-label="锚点">#</a>
    </span>
    
</h5>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># AI 生成的代码</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">update_user_email_task</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">repo</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">repo</span><span class="o">.</span><span class="n">update_email</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 忘记 await session.commit() ← 更新不会落库</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">"status"</span><span class="p">:</span> <span class="s2">"success"</span><span class="p">}</span></span></span></code></pre></div></div>
<p><strong>后果</strong>：数据库状态未持久化，任务状态丢失</p>

<h5 class="relative group">3. 忽略项目约定 — 不知道特殊规则
    <div id="3-忽略项目约定--不知道特殊规则" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e5%bf%bd%e7%95%a5%e9%a1%b9%e7%9b%ae%e7%ba%a6%e5%ae%9a--%e4%b8%8d%e7%9f%a5%e9%81%93%e7%89%b9%e6%ae%8a%e8%a7%84%e5%88%99" aria-label="锚点">#</a>
    </span>
    
</h5>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># AI 在 CLI 文件里写了全局导入</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">src.domain.users.service</span> <span class="kn">import</span> <span class="n">UserService</span>  <span class="c1"># ❌ 违反规则</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 应该用局部导入</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_user</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="kn">from</span> <span class="nn">src.domain.users.service</span> <span class="kn">import</span> <span class="n">UserService</span>  <span class="c1"># ✅</span></span></span></code></pre></div></div>
<p><strong>后果</strong>：CLI 启动慢，循环依赖风险</p>
<hr>
<p><strong>应对策略</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">错误类型</th>
          <th style="text-align: left">检测方式</th>
          <th style="text-align: center">自动化程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">静态问题（语法/风格/禁令）</td>
          <td style="text-align: left">ruff + ast-grep（pre-commit）</td>
          <td style="text-align: center">✅ 全自动</td>
      </tr>
      <tr>
          <td style="text-align: left">行为问题（遗漏调用/边界条件）</td>
          <td style="text-align: left">pytest（单元/集成）</td>
          <td style="text-align: center">⚠️ 需要补测试</td>
      </tr>
      <tr>
          <td style="text-align: left">运行时问题（性能/安全）</td>
          <td style="text-align: left">运行时数据 + 人工审查</td>
          <td style="text-align: center">❌ 高成本</td>
      </tr>
  </tbody>
</table>
<p><strong>原则</strong>：能自动拦截的，不靠人工 review。</p>
<hr>

<h3 class="relative group">五、行动清单
    <div id="五行动清单" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%ba%94%e8%a1%8c%e5%8a%a8%e6%b8%85%e5%8d%95" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">立即可做（1天内）
    <div id="立即可做1天内" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ab%8b%e5%8d%b3%e5%8f%af%e5%81%9a1%e5%a4%a9%e5%86%85" aria-label="锚点">#</a>
    </span>
    
</h4>
<ul>
<li><input disabled="" type="checkbox"> 创建 CLAUDE.md，写清技术栈、架构、规范</li>
<li><input disabled="" type="checkbox"> 固化最小验证闭环（例如 <code>just lint</code> / <code>just test</code>），让“跑一遍验证”成本接近 0</li>
<li><input disabled="" type="checkbox"> 配置 pre-commit hooks（ruff + ast-grep + Conventional Commits 校验 + pre-push pytest）</li>
<li><input disabled="" type="checkbox"> 识别项目中的甜点区任务（CRUD / 测试 / 迁移 / 配置）</li>
</ul>

<h4 class="relative group">中期投入（1周内）
    <div id="中期投入1周内" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ad%e6%9c%9f%e6%8a%95%e5%85%a51%e5%91%a8%e5%86%85" aria-label="锚点">#</a>
    </span>
    
</h4>
<ul>
<li><input disabled="" type="checkbox"> 写 1-3 条最痛的 ast-grep 规则，拦截项目特定错误（零容忍）</li>
<li><input disabled="" type="checkbox"> 建立提示词模板库</li>
<li><input disabled="" type="checkbox"> 完善测试覆盖率</li>
</ul>

<h4 class="relative group">长期建设
    <div id="长期建设" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%95%bf%e6%9c%9f%e5%bb%ba%e8%ae%be" aria-label="锚点">#</a>
    </span>
    
</h4>
<ul>
<li><input disabled="" type="checkbox"> 持续更新 CLAUDE.md</li>
<li><input disabled="" type="checkbox"> 积累甜点区和泥潭案例</li>
<li><input disabled="" type="checkbox"> 团队分享最佳实践</li>
</ul>
<hr>

<h3 class="relative group">六、房间里的大象
    <div id="六房间里的大象" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%ad%e6%88%bf%e9%97%b4%e9%87%8c%e7%9a%84%e5%a4%a7%e8%b1%a1" aria-label="锚点">#</a>
    </span>
    
</h3>
<blockquote><p>如果你只关心“怎么落地”，建议先看「五、行动清单」和「附录 A：本仓库可复用资产」。</p>
</blockquote><p>我知道有人听完会想：</p>
<blockquote><p>“如果代码都 AI 写了，我算什么？”</p>
</blockquote><p>这个问题值得正面回答。</p>

<h4 class="relative group">6.1 你的担忧是合理的
    <div id="61-你的担忧是合理的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#61-%e4%bd%a0%e7%9a%84%e6%8b%85%e5%bf%a7%e6%98%af%e5%90%88%e7%90%86%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h4>
<table>
  <thead>
      <tr>
          <th style="text-align: left">担忧</th>
          <th style="text-align: left">回应</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">“主要由 AI 生成"是不是 KPI 营销？</td>
          <td style="text-align: left">是，也不是。Prompt 工程、需求拆解、架构决策、验收验证——这些都是编程，只是换了语言。“主要由 AI 生成"强调的是"不手写代码”，不是"人不参与”。</td>
      </tr>
      <tr>
          <td style="text-align: left">Junior 怎么练手？</td>
          <td style="text-align: left">可能更需要通过阅读优秀开源项目、复盘 AI 的错误来反向学习，而不是靠写 CRUD 堆时长。</td>
      </tr>
      <tr>
          <td style="text-align: left">Review AI 代码很累</td>
          <td style="text-align: left">对。读别人的代码本来就累，读 AI 的更累——因为它"看起来对"。40% 时间在验，不是吹的。</td>
      </tr>
      <tr>
          <td style="text-align: left">效率提升 = 裁员信号？</td>
          <td style="text-align: left">短期是优化人效，长期取决于业务是否扩张。</td>
      </tr>
  </tbody>
</table>

<h4 class="relative group">6.2 几点澄清
    <div id="62-几点澄清" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#62-%e5%87%a0%e7%82%b9%e6%be%84%e6%b8%85" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><strong>坏了，我成替身了</strong></p>
<p>换个视角：你不是 AI 的秘书，<strong>AI 是你的实习生</strong>。</p>
<ul>
<li>实习生能干活，但你得告诉他干什么</li>
<li>实习生会犯错，你得 Review</li>
<li>实习生干得好，功劳算你的</li>
</ul>
<p>区别是：这个实习生不睡觉、不抱怨、不要工资，而且你可以同时带 10 个。</p>
<p><strong>门槛降低了吗？</strong></p>
<p>没有。门槛<strong>转移</strong>了——从"会写代码"转移到"理解系统 + 会验证"。</p>
<p>讽刺的是：<strong>AI 时代对工程师的要求更高了，不是更低了</strong>。</p>

<h4 class="relative group">6.3 最诚实的回答
    <div id="63-最诚实的回答" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#63-%e6%9c%80%e8%af%9a%e5%ae%9e%e7%9a%84%e5%9b%9e%e7%ad%94" aria-label="锚点">#</a>
    </span>
    
</h4>
<blockquote><p>“如果我还没成为架构师，是不是就直接被淘汰了？”</p>
</blockquote><p>诚实的回答：<strong>可能是</strong>。</p>
<p>但这不是 AI 带来的新问题——这是软件行业一直存在的问题：</p>
<ul>
<li>35 岁危机</li>
<li>“写 CRUD 能写一辈子吗”</li>
<li>“不往上走就等着被优化”</li>
</ul>
<p>AI 只是把这个问题<strong>加速暴露</strong>了。</p>

<h4 class="relative group">6.4 如果你感到焦虑
    <div id="64-如果你感到焦虑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#64-%e5%a6%82%e6%9e%9c%e4%bd%a0%e6%84%9f%e5%88%b0%e7%84%a6%e8%99%91" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1. 承认焦虑是合理的
</span></span><span class="line"><span class="cl">2. 但焦虑不解决问题
</span></span><span class="line"><span class="cl">3. 要么卷（学 Prompt、学架构、往上走）
</span></span><span class="line"><span class="cl">4. 要么换赛道（AI 不擅长的领域）
</span></span><span class="line"><span class="cl">5. 要么接受（成为"AI 协作者"，放下对"手写代码"的执念）</span></span></code></pre></div></div>
<p><strong>今天的分享不是要告诉你"你必须这样做"，而是分享一种可能性。</strong></p>
<p><strong>工具在变，但我们解决问题的核心价值没变。希望大家都能成为那个驾驭 AI 的人，而不是被 AI 驾驭的人。</strong></p>

<h4 class="relative group">6.5 认知带宽：那个没人说的瓶颈
    <div id="65-认知带宽那个没人说的瓶颈" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#65-%e8%ae%a4%e7%9f%a5%e5%b8%a6%e5%ae%bd%e9%82%a3%e4%b8%aa%e6%b2%a1%e4%ba%ba%e8%af%b4%e7%9a%84%e7%93%b6%e9%a2%88" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>前面说"40% 时间在验"，但没说清楚：<strong>为什么验这么累？</strong></p>
<p>答案是：<strong>算力可扩展，认知带宽不可扩展。</strong></p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">维度</th>
          <th style="text-align: left">AI (硅基)</th>
          <th style="text-align: left">人 (碳基)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">上下文窗口</td>
          <td style="text-align: left">4K → 1M token</td>
          <td style="text-align: left">7±2 单位（锁死）</td>
      </tr>
      <tr>
          <td style="text-align: left">切换成本</td>
          <td style="text-align: left">毫秒级</td>
          <td style="text-align: left">分钟级 + 精神力损耗</td>
      </tr>
      <tr>
          <td style="text-align: left">能源</td>
          <td style="text-align: left">电（便宜）</td>
          <td style="text-align: left">多巴胺（不可再生）</td>
      </tr>
  </tbody>
</table>
<p><strong>后果</strong>：</p>
<ul>
<li>AI 一秒吐出 3000 行代码，你要用 7 个单位的缓存去审</li>
<li>每次被打断（模型报错/追问/工具弹窗），你的"架构思路 KV Cache"就被清空</li>
<li>一天下来，没写几行代码，却比写 1000 行还累——这叫 <strong>Decision Fatigue</strong></li>
</ul>
<p><strong>应对策略</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">策略</th>
          <th style="text-align: left">做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">异步化</td>
          <td style="text-align: left">任务丢进队列，批处理结果，不要实时盯着 AI 输出</td>
      </tr>
      <tr>
          <td style="text-align: left">AI 过滤 AI</td>
          <td style="text-align: left">让另一个模型先做 Review/风险摘要，你只看高风险点</td>
      </tr>
      <tr>
          <td style="text-align: left">降噪</td>
          <td style="text-align: left">要求 AI 输出 TL;DR，不看中间思考过程</td>
      </tr>
  </tbody>
</table>
<p><strong>一句话</strong>：效率提升有天花板，天花板就是你的生物学极限。</p>
<blockquote><p>硅基把 CPU 烧红了，换个散热器就行。
碳基把精神力烧干了，那叫 Burnout，重启都难。</p>
</blockquote><hr>

<h3 class="relative group">附录 A：本仓库可复用资产（落地对照）
    <div id="附录-a本仓库可复用资产落地对照" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%99%84%e5%bd%95-a%e6%9c%ac%e4%bb%93%e5%ba%93%e5%8f%af%e5%a4%8d%e7%94%a8%e8%b5%84%e4%ba%a7%e8%90%bd%e5%9c%b0%e5%af%b9%e7%85%a7" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这套方法论最怕“听懂了，但落不了地”。本仓库把关键资产都固化成了文件与命令：</p>
<ul>
<li><code>CLAUDE.md</code>：项目级上下文（技术栈、目录结构、零容忍规则、常用命令）</li>
<li><code>.pre-commit-config.yaml</code>：提交前质量门禁（ruff/ast-grep/commit-msg 校验等）</li>
<li><code>sgconfig.yml</code> + <code>ast-grep/rules/</code>：项目特定规则（把“隐含约定”变成“可执行约束”）</li>
<li><code>justfile</code>：把常用验证命令固化成 <code>just lint</code> / <code>just test</code></li>
<li><code>docs/ai-collaboration.md</code>：提示词模板与任务分区（甜点区/谨慎区/止损线）</li>
</ul>
<p>一个最小闭环（示例）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> .venv/bin/activate
</span></span><span class="line"><span class="cl">uv sync --group<span class="o">=</span>dev
</span></span><span class="line"><span class="cl">just lint
</span></span><span class="line"><span class="cl">just test</span></span></code></pre></div></div>
<blockquote><p>注：如果测试依赖数据库/缓存等外部服务，请按 <code>README.md</code> / <code>CLAUDE.md</code> 完成环境配置后再运行。</p>
</blockquote><hr>

<h3 class="relative group">附录 B：多模型协作（抽象版）
    <div id="附录-b多模型协作抽象版" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%99%84%e5%bd%95-b%e5%a4%9a%e6%a8%a1%e5%9e%8b%e5%8d%8f%e4%bd%9c%e6%8a%bd%e8%b1%a1%e7%89%88" aria-label="锚点">#</a>
    </span>
    
</h3>
<pre class="not-prose mermaid">graph TD
    Human((人<br/>Architect + Reviewer))

    subgraph 输入层
        Notes[语音/笔记/需求文档] --> Human
    end

    subgraph 决策层
        Human -->|方案评审/拆解| Planner[LLM<br/>Architecture Review]
    end

    subgraph 执行层
        Planner -->|实现方案| Coder[LLM<br/>Coding + Tests]
        Human -->|Context/CLAUDE.md| Coder
    end

    subgraph 验证层
        Coder --> Gate[ruff + ast-grep + pytest]
        Gate --> Human
    end</pre>
<p><strong>核心原则</strong>：人是决策节点，AI 是执行层；验证体系是信任的前提。</p>

<h3 class="relative group">附录 C：关于本文档（一种自举）
    <div id="附录-c关于本文档一种自举" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%99%84%e5%bd%95-c%e5%85%b3%e4%ba%8e%e6%9c%ac%e6%96%87%e6%a1%a3%e4%b8%80%e7%a7%8d%e8%87%aa%e4%b8%be" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>本文档由 AI 生成 —— 这本身就验证了文档的核心论点：</p>
<blockquote><p>代码不用写了，但需求、架构、验证——一个都不能少。</p>
</blockquote><p><strong>分工</strong>：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">角色</th>
          <th style="text-align: left">贡献</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">人</td>
          <td style="text-align: left">论点、结构、案例筛选、审校</td>
      </tr>
      <tr>
          <td style="text-align: left">AI</td>
          <td style="text-align: left">内容生成、格式排版、润色</td>
      </tr>
  </tbody>
</table>
<p><strong>用 AI 写「如何用 AI」，是最好的证明。</strong></p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">31</span><span class="nx">T21</span><span class="o">:</span><span class="mi">46</span><span class="o">:</span><span class="mi">31</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T03</span><span class="o">:</span><span class="mi">58</span><span class="o">:</span><span class="mi">25</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/94
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>多层隧道 MTU 黑洞排查：scp 跑 30KB/s 的玄学</title>
      <link>https://blog.ferstar.org/posts/mtu-black-hole-scp-optimization/</link>
      <pubDate>Wed, 31 Dec 2025 12:09:54 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/mtu-black-hole-scp-optimization/</guid>
      <description>禁 ICMP 的隧道环境下 scp 速度掉到 30KB/s；用 MTU 探测与 TCP MSS Clamping 处理；恢复多层隧道的正常吞吐。</description><content:encoded><![CDATA[<blockquote><p>AI 协助编写</p>
</blockquote>
<h3 class="relative group">问题
    <div id="问题" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%97%ae%e9%a2%98" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>跨地域算力池部署，Site-A 和 Site-B 之间跑了两层隧道：<strong>WireGuard + IPsec</strong>。安全合规要求，<strong>全站禁 ICMP</strong>——<code>ping</code> 和 <code>traceroute</code> 全废。</p>
<p>然后诡异的事情来了：</p>
<ul>
<li><strong>A → B</strong>：50Mbps，跑满，没毛病</li>
<li><strong>B → A</strong>：SSH 能连，但 <code>scp</code> 大文件直接掉到 <strong>30KB/s</strong></li>
</ul>
<p>典型的"单行道"症状。链路没断，但某个协议层面的天花板被撞了。</p>
<p>一开始怀疑运营商限速、偷包，甚至想过是不是 QoS 策略在搞鬼。折腾半天，压根没往 MTU 方向想——典型的专家盲区：手里拿着抓包工具，看什么都像协议问题，反而忽略了最基础的链路参数。就像网络高手帮朋友排查问题，<code>tcpdump</code>、<code>wireshark</code> 一顿操作，最后发现是宽带欠费了。</p>
<hr>

<h3 class="relative group">排查：没 ICMP 怎么玩？
    <div id="排查没-icmp-怎么玩" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8e%92%e6%9f%a5%e6%b2%a1-icmp-%e6%80%8e%e4%b9%88%e7%8e%a9" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>正常情况用 <code>ping -s <size> -M do</code> 探测路径 MTU。但 ICMP 被封了，收不到任何"包太大"的反馈。</p>
<p>只能靠 <strong>TCP 行为指纹</strong> 推断。用 <code>iperf3</code> 做对比实验：</p>

<h4 class="relative group">1) TCP 默认测试
    <div id="1-tcp-默认测试" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-tcp-%e9%bb%98%e8%ae%a4%e6%b5%8b%e8%af%95" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 接收端</span>
</span></span><span class="line"><span class="cl">iperf3 -s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 发送端</span>
</span></span><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -t <span class="m">10</span></span></span></code></pre></div></div>
<p>现象：<code>Retr</code> 重传爆炸，带宽锁死在 ~0.4Mbps。</p>

<h4 class="relative group">2) UDP 对照
    <div id="2-udp-对照" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-udp-%e5%af%b9%e7%85%a7" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -u -b 20M -t <span class="m">10</span></span></span></code></pre></div></div>
<p>现象：20Mbps 基本正常，丢包可控。说明链路本身没挂。</p>

<h4 class="relative group">3) TCP MSS 步进探测（关键）
    <div id="3-tcp-mss-步进探测关键" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-tcp-mss-%e6%ad%a5%e8%bf%9b%e6%8e%a2%e6%b5%8b%e5%85%b3%e9%94%ae" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -t <span class="m">10</span> -M <span class="m">1400</span>  <span class="c1"># 坍缩，重传爆炸</span>
</span></span><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -t <span class="m">10</span> -M <span class="m">1350</span>  <span class="c1"># 瞬间恢复 ~49Mbps</span></span></span></code></pre></div></div>
<p><strong>破案</strong>：典型的 <strong>PMTUD 黑洞</strong>，在"致盲"环境下触发。</p>
<hr>

<h3 class="relative group">原理：谁在"谋杀"数据包？
    <div id="原理谁在谋杀数据包" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%9f%e7%90%86%e8%b0%81%e5%9c%a8%e8%b0%8b%e6%9d%80%e6%95%b0%e6%8d%ae%e5%8c%85" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">MSS/MTU 换算
    <div id="mssmtu-换算" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#mssmtu-%e6%8d%a2%e7%ae%97" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>TCP MSS = 单个 TCP 段的 payload 上限，受路径 MTU 限制。</p>
<ul>
<li>IPv4 近似：<code>MSS ≈ MTU - 20(IP) - 20(TCP) - TCP_OPTIONS</code></li>
<li>以太网 MTU 1500 时，默认 MSS 约 1460</li>
</ul>
<p>多层隧道（WireGuard + IPsec）下，封装头部持续"吃掉"有效 MTU，默认大 MSS 就变成"超载包"。</p>

<h4 class="relative group">为什么变成黑洞？
    <div id="为什么变成黑洞" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%8f%98%e6%88%90%e9%bb%91%e6%b4%9e" aria-label="锚点">#</a>
    </span>
    
</h4>
<ol>
<li>发送方按默认 MSS 发大包</li>
<li>中间节点发现包太大，本该回 ICMP（Fragmentation Needed / Packet Too Big）</li>
<li>但 <strong>全站禁 ICMP</strong>，这个关键信号被防火墙吃了</li>
<li>发送方收不到通知，误判为"链路丢包"，疯狂重传同样大小的包</li>
<li>吞吐被重传拖死</li>
</ol>
<p>这就是 <strong>MTU 黑洞</strong>——包丢了，但没人告诉你为什么。</p>

<h4 class="relative group">隧道开销（别迷信固定数字）
    <div id="隧道开销别迷信固定数字" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%9a%a7%e9%81%93%e5%bc%80%e9%94%80%e5%88%ab%e8%bf%b7%e4%bf%a1%e5%9b%ba%e5%ae%9a%e6%95%b0%e5%ad%97" aria-label="锚点">#</a>
    </span>
    
</h4>
<ul>
<li>WireGuard：~60-80 bytes（取决于 IPv4/IPv6、UDP、实现细节）</li>
<li>IPsec ESP：~50-90 bytes（取决于 transport/tunnel 模式、NAT-T、padding、加密套件）</li>
</ul>
<p><strong>靠谱做法</strong>：抓包确认实际封装开销，倒推安全 MTU/MSS。</p>
<hr>

<h3 class="relative group">解决方案
    <div id="解决方案" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">A. 根治（理想情况）
    <div id="a-根治理想情况" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#a-%e6%a0%b9%e6%b2%bb%e7%90%86%e6%83%b3%e6%83%85%e5%86%b5" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>不是放行 <code>ping</code>，而是只放行 PMTUD 必要的 ICMP：</p>
<ul>
<li>IPv4：ICMP Type 3 Code 4（Fragmentation Needed）</li>
<li>IPv6：ICMPv6 Type 2（Packet Too Big）</li>
</ul>
<p>这样 TCP 能自动收敛到正确 MTU，不用硬编码 MSS。</p>
<p><strong>但现实是</strong>：这类安全策略往往不归你管，合规要求动不了。往下看。</p>

<h4 class="relative group">B. 权宜 1：降隧道 MTU
    <div id="b-权宜-1降隧道-mtu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#b-%e6%9d%83%e5%ae%9c-1%e9%99%8d%e9%9a%a7%e9%81%93-mtu" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>WireGuard/IPsec 嵌套场景，保守降接口 MTU，立刻止血。代价是 payload 利用率略降。</p>

<h4 class="relative group">C. 权宜 2：MSS 钳制（我用的方案）
    <div id="c-权宜-2mss-钳制我用的方案" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c-%e6%9d%83%e5%ae%9c-2mss-%e9%92%b3%e5%88%b6%e6%88%91%e7%94%a8%e7%9a%84%e6%96%b9%e6%a1%88" aria-label="锚点">#</a>
    </span>
    
</h4>
<p>既然 ICMP 指望不上，直接在中转网关上暴力改写 SYN 包的 MSS：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- 配置 ---</span>
</span></span><span class="line"><span class="cl"><span class="nv">PHY_IF</span><span class="o">=</span><span class="s2">"ens3"</span>
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_NET</span><span class="o">=</span><span class="s2">"10.0.0.0/24"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. 物理层优化（会增加 CPU 开销，按需开）</span>
</span></span><span class="line"><span class="cl">ip link <span class="nb">set</span> dev <span class="s2">"</span><span class="nv">$PHY_IF</span><span class="s2">"</span> mtu <span class="m">1400</span>
</span></span><span class="line"><span class="cl">ethtool -K <span class="s2">"</span><span class="nv">$PHY_IF</span><span class="s2">"</span> tso off gso off gro off
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. MSS 钳制</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 转发流量：1280（保守）</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -C FORWARD -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1280</span> 2>/dev/null <span class="o">||</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">iptables -t mangle -I FORWARD -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1280</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 本地发起：1350（略激进但本案例可用）</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -C OUTPUT -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1350</span> 2>/dev/null <span class="o">||</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">iptables -t mangle -I OUTPUT -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1350</span></span></span></code></pre></div></div>
<blockquote><p>纯 nftables 环境需要用等价 nft 规则。</p>
</blockquote><hr>

<h3 class="relative group">验证
    <div id="验证" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%aa%8c%e8%af%81" aria-label="锚点">#</a>
    </span>
    
</h3>

<h4 class="relative group">1) 确认 MSS 被改写
    <div id="1-确认-mss-被改写" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e7%a1%ae%e8%ae%a4-mss-%e8%a2%ab%e6%94%b9%e5%86%99" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -ni <IFACE> -vv <span class="s1">'tcp[tcpflags] & tcp-syn != 0 and host <REMOTE_HOST>'</span></span></span></code></pre></div></div>

<h4 class="relative group">2) 确认规则命中
    <div id="2-确认规则命中" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e7%a1%ae%e8%ae%a4%e8%a7%84%e5%88%99%e5%91%bd%e4%b8%ad" aria-label="锚点">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iptables -t mangle -vnL FORWARD --line-numbers
</span></span><span class="line"><span class="cl">iptables -t mangle -vnL OUTPUT  --line-numbers</span></span></code></pre></div></div>

<h4 class="relative group">3) 实测
    <div id="3-实测" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e5%ae%9e%e6%b5%8b" aria-label="锚点">#</a>
    </span>
    
</h4>
<p><code>iperf3</code> 或 <code>scp</code> 回归，<code>Retr</code> 显著下降，吞吐恢复。</p>
<hr>

<h3 class="relative group">回滚
    <div id="回滚" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9b%9e%e6%bb%9a" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 删 mangle 规则（按行号）</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -D FORWARD <行号>
</span></span><span class="line"><span class="cl">iptables -t mangle -D OUTPUT <行号>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 恢复 MTU/offload</span>
</span></span><span class="line"><span class="cl">ip link <span class="nb">set</span> dev <IFACE> mtu <span class="m">1500</span>
</span></span><span class="line"><span class="cl">ethtool -K <IFACE> tso on gso on gro on</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">总结
    <div id="总结" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%bb%e7%bb%93" aria-label="锚点">#</a>
    </span>
    
</h3>
<ol>
<li><strong>MTU 是第一生产力</strong>：VPN/Overlay 场景，默认 1500 就是坑</li>
<li><strong>非对称性思维</strong>：单向限速通常不是带宽不够，而是包大小不对</li>
<li><strong>宁缺毋滥</strong>：MSS 设保守点，牺牲一点载荷效率，换数量级的稳定性提升</li>
</ol>
<p>在"无声"的网络里排障，理解协议头比会用工具更重要。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">31</span><span class="nx">T12</span><span class="o">:</span><span class="mi">09</span><span class="o">:</span><span class="mi">54</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T06</span><span class="o">:</span><span class="mi">10</span><span class="o">:</span><span class="mi">01</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/93
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Linux 6.17 网络优化：DualPI2 &#43; BBR 彻底解决 50Mbps 窄带宽 Bufferbloat</title>
      <link>https://blog.ferstar.org/posts/linux-network-bbr-dualpi2/</link>
      <pubDate>Thu, 18 Dec 2025 06:30:04 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/linux-network-bbr-dualpi2/</guid>
      <description>窄带宽大下载时延迟暴涨；用 Linux 6.17 的 DualPI2 + BBR 调优；让 SSH 与交互流量在满载下依然顺滑。</description><content:encoded><![CDATA[
<h2 class="relative group">背景
    <div id="背景" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%83%8c%e6%99%af" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>在一台 50Mbps 带宽的云主机上部署 VPN 中转服务时，遇到了经典的 <strong>Bufferbloat</strong>（缓冲区膨胀）问题：一旦有大文件下载占满带宽，SSH 延迟从 50ms 飙升到 200ms+，严重影响交互体验。</p>
<p>传统的 <code>fq</code> + <code>bbr</code> 组合在窄带宽场景下效果有限，因为 <code>fq</code> 只做公平队列，无法主动控制排队延迟。</p>

<h2 class="relative group">方案：DualPI2 + BBR
    <div id="方案dualpi2--bbr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%96%b9%e6%a1%88dualpi2--bbr" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>Linux 6.17 内核引入了 <code>sch_dualpi2</code> 调度器，专为低延迟设计：</p>
<table>
  <thead>
      <tr>
          <th>调度器</th>
          <th>工作方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fq</code></td>
          <td>公平队列，让每个连接平分带宽，但不管排队延迟</td>
      </tr>
      <tr>
          <td><code>dualpi2</code></td>
          <td>主动管理排队时间，通过 ECN 标记/丢包强制发送端降速，死守延迟底线</td>
      </tr>
  </tbody>
</table>

<h2 class="relative group">配置步骤
    <div id="配置步骤" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%85%8d%e7%bd%ae%e6%ad%a5%e9%aa%a4" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">1. 内核要求
    <div id="1-内核要求" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e5%86%85%e6%a0%b8%e8%a6%81%e6%b1%82" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>需要 Linux 6.17+ 内核（包含 <code>sch_dualpi2</code> 模块）。</p>

<h3 class="relative group">2. sysctl 配置
    <div id="2-sysctl-配置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-sysctl-%e9%85%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>编辑 <code>/etc/sysctl.conf</code>：</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-conf" data-lang="conf"># --- 核心调度方案 ---
net.core.default_qdisc = dualpi2
net.ipv4.tcp_congestion_control = bbr

# --- 极致延迟优化 ---
net.ipv4.tcp_ecn = 1              # 必须开启，dualpi2 核心手段
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_recovery = 1         # RACK 快速修复乱序
net.ipv4.tcp_notsent_lowat = 8192 # 降低 BBR 内核缓冲量

# --- 缓冲区设置（针对 50Mbps，严防膨胀）---
net.core.rmem_max = 4194304
net.core.wmem_max = 4194304
net.ipv4.tcp_rmem = 4096 16384 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304

# --- 稳定性优化 ---
net.core.somaxconn = 2048
net.ipv4.tcp_max_syn_backlog = 2048
net.mptcp.enabled = 1</code></pre></div>

<h3 class="relative group">3. 模块自动加载
    <div id="3-模块自动加载" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e6%a8%a1%e5%9d%97%e8%87%aa%e5%8a%a8%e5%8a%a0%e8%bd%bd" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"sch_dualpi2"</span> <span class="p">|</span> sudo tee /etc/modules-load.d/dualpi2.conf</span></span></code></pre></div></div>

<h3 class="relative group">4. 网卡持久化（udev 规则）
    <div id="4-网卡持久化udev-规则" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-%e7%bd%91%e5%8d%a1%e6%8c%81%e4%b9%85%e5%8c%96udev-%e8%a7%84%e5%88%99" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>创建 <code>/etc/udev/rules.d/99-ens3-dualpi2.rules</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">ACTION</span><span class="o">==</span><span class="s2">"add"</span>, <span class="nv">SUBSYSTEM</span><span class="o">==</span><span class="s2">"net"</span>, <span class="nv">NAME</span><span class="o">==</span><span class="s2">"ens3"</span>, <span class="nv">RUN</span><span class="o">+=</span><span class="s2">"/sbin/tc qdisc replace dev ens3 root dualpi2"</span></span></span></code></pre></div></div>
<blockquote><p>将 <code>ens3</code> 替换为你的网卡名</p>
</blockquote>
<h3 class="relative group">5. 应用配置
    <div id="5-应用配置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-%e5%ba%94%e7%94%a8%e9%85%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sysctl -p
</span></span><span class="line"><span class="cl">sudo tc qdisc replace dev ens3 root dualpi2</span></span></code></pre></div></div>

<h2 class="relative group">验证方法
    <div id="验证方法" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%aa%8c%e8%af%81%e6%96%b9%e6%b3%95" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">检查 dualpi2 状态
    <div id="检查-dualpi2-状态" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%a3%80%e6%9f%a5-dualpi2-%e7%8a%b6%e6%80%81" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tc -s qdisc show dev ens3
</span></span><span class="line"><span class="cl"><span class="c1"># 观察 target 值，默认 5ms</span></span></span></code></pre></div></div>

<h3 class="relative group">压力测试（服务器禁 Ping 方案）
    <div id="压力测试服务器禁-ping-方案" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%8b%e5%8a%9b%e6%b5%8b%e8%af%95%e6%9c%8d%e5%8a%a1%e5%99%a8%e7%a6%81-ping-%e6%96%b9%e6%a1%88" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>传统 <code>ping</code> 在禁 ICMP 的服务器上失效，改用内核 RTT 统计：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 客户端：打满带宽</span>
</span></span><span class="line"><span class="cl">iperf3 -c <server> -t <span class="m">60</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 服务端：观察内核 RTT</span>
</span></span><span class="line"><span class="cl">ss -tni
</span></span><span class="line"><span class="cl"><span class="c1"># 关注 rtt:X/Y — X 是平均 RTT，Y 是抖动(mdev)</span></span></span></code></pre></div></div>

<h2 class="relative group">测试结果
    <div id="测试结果" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b5%8b%e8%af%95%e7%bb%93%e6%9e%9c" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>在 50Mbps 带宽满载情况下：</p>
<table>
  <thead>
      <tr>
          <th>指标</th>
          <th>结果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>满载带宽</td>
          <td>50 Mbps（峰值 51.4 Mbps）</td>
      </tr>
      <tr>
          <td>满载时 SSH RTT</td>
          <td>54.78ms（物理极限 48.33ms）</td>
      </tr>
      <tr>
          <td>RTT 抖动</td>
          <td><strong>2.08ms</strong> ✅</td>
      </tr>
      <tr>
          <td>拥塞控制</td>
          <td>全连接 BBR 生效</td>
      </tr>
      <tr>
          <td>丢包恢复</td>
          <td>RACK 快速修复，无拥塞崩溃</td>
      </tr>
  </tbody>
</table>
<p><strong>满载下载时 SSH 延迟仅高出物理极限 6.4ms，抖动 2ms，Bufferbloat 完全控制。</strong></p>

<h2 class="relative group">原理解析
    <div id="原理解析" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%9f%e7%90%86%e8%a7%a3%e6%9e%90" aria-label="锚点">#</a>
    </span>
    
</h2>
<ol>
<li><strong>DualPI2</strong> 在微秒级压制排队延迟，默认 target 5ms</li>
<li><strong>BBR</strong> 在毫秒级压制带宽膨胀，周期性探测真实 RTT</li>
<li><strong>ECN</strong> 让发送端提前感知拥塞，避免丢包</li>
<li><strong>RACK</strong> 快速修复乱序，避免拥塞窗口崩溃</li>
</ol>
<p>这套组合在 50Mbps 窄带宽下实现了近乎完美的延迟/吞吐平衡。</p>

<h2 class="relative group">参考
    <div id="参考" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8f%82%e8%80%83" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li><a href="https://datatracker.ietf.org/doc/draft-ietf-tsvwg-aqm-dualq-coupled/"  target="_blank" rel="noreferrer">DualPI2 - IETF Draft</a></li>
<li><a href="https://github.com/google/bbr"  target="_blank" rel="noreferrer">BBR Congestion Control</a></li>
<li><a href="https://datatracker.ietf.org/doc/rfc9332/"  target="_blank" rel="noreferrer">sch_dualpi2</a></li>
</ul>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">18</span><span class="nx">T06</span><span class="o">:</span><span class="mi">30</span><span class="o">:</span><span class="mi">04</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">18</span><span class="nx">T12</span><span class="o">:</span><span class="mi">06</span><span class="o">:</span><span class="mi">30</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/92
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>2025年了，为什么我还要root？</title>
      <link>https://blog.ferstar.org/posts/why-rooting-still-matters-in-2025/</link>
      <pubDate>Sat, 04 Oct 2025 14:42:47 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/why-rooting-still-matters-in-2025/</guid>
      <description>厂商限制与系统臃肿让性能与隐私难控；通过 root、内核与系统级调优解决；获得更快、更干净、更可控的使用体验。</description><content:encoded><![CDATA[<p>先列个单子，慢填（排名不分先后）</p>

<h2 class="relative group">1. 我希望有新的内核特性，榨干硬件性能
    <div id="1-我希望有新的内核特性榨干硬件性能" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e6%88%91%e5%b8%8c%e6%9c%9b%e6%9c%89%e6%96%b0%e7%9a%84%e5%86%85%e6%a0%b8%e7%89%b9%e6%80%a7%e6%a6%a8%e5%b9%b2%e7%a1%ac%e4%bb%b6%e6%80%a7%e8%83%bd" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>厂商为了稳定性通常用老内核，但新内核有不少好东西：</p>
<ul>
<li>BBRv2 拥塞控制，联通4G网速提升明显</li>
<li>f2fs 文件系统优化，配合 UFS 3.1 随机读写有感提升</li>
<li>zstd 压缩的 ZRAM，压缩比比 lz4 高 20%+</li>
<li>自定义 CPU 调度器（试过 EAS），日常使用功耗降低 15% 左右</li>
</ul>
<p>实际编译过 xaga 和 RMX3888 的内核，开了一堆厂商关掉的 feature，配合 KernelSU 比 Magisk 稳定。</p>
<p>工具：Franco Kernel Manager / UKMM</p>

<h2 class="relative group">2. 干掉云控，防止厂商施法负优化
    <div id="2-干掉云控防止厂商施法负优化" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e5%b9%b2%e6%8e%89%e4%ba%91%e6%8e%a7%e9%98%b2%e6%ad%a2%e5%8e%82%e5%95%86%e6%96%bd%e6%b3%95%e8%b4%9f%e4%bc%98%e5%8c%96" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>厂商最恶心的几个操作：</p>
<ul>
<li>OTA 后偷偷改配置（120Hz 变回 60Hz）</li>
<li>游戏识别后CPU降频（明明温度还低）</li>
<li>强推广告和全家桶app</li>
<li>收集隐私数据上传</li>
</ul>
<p>我的做法：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 冻结系统更新</span>
</span></span><span class="line"><span class="cl">pm disable com.xxx.ota
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># hosts 屏蔽云控域名</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"127.0.0.1 cloud.manufacturer.com"</span> >> /system/etc/hosts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># LSPosed hook 系统服务拦截配置下发</span></span></span></code></pre></div></div>
<p>实用模块：CorePatch（绕签名校验）、Shamiko（隐藏root）</p>

<h2 class="relative group">3. 不想被特定生态绑定，迫不得已需要对某些app伪装机型
    <div id="3-不想被特定生态绑定迫不得已需要对某些app伪装机型" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e4%b8%8d%e6%83%b3%e8%a2%ab%e7%89%b9%e5%ae%9a%e7%94%9f%e6%80%81%e7%bb%91%e5%ae%9a%e8%bf%ab%e4%b8%8d%e5%be%97%e5%b7%b2%e9%9c%80%e8%a6%81%e5%af%b9%e6%9f%90%e4%ba%9bapp%e4%bc%aa%e8%a3%85%e6%9c%ba%e5%9e%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>某些银行app限制机型（尤其国际版ROM），某些游戏锁区域，还有薅羊毛限定机型的</p>
<p>技术上就是 hook <code>android.os.Build</code> 类：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">XposedHelpers</span><span class="p">.</span><span class="na">setStaticObjectField</span><span class="p">(</span><span class="n">Build</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="s">"MODEL"</span><span class="p">,</span><span class="w"> </span><span class="s">"SM-G9980"</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">XposedHelpers</span><span class="p">.</span><span class="na">setStaticObjectField</span><span class="p">(</span><span class="n">Build</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="s">"MANUFACTURER"</span><span class="p">,</span><span class="w"> </span><span class="s">"samsung"</span><span class="p">);</span></span></span></code></pre></div></div>
<p>或者用现成的模块：Device Faker / Hide My Applist</p>

<h2 class="relative group">4. 自动化记账（OCR、无障碍这种稳定性太差了，lsposed hook才是王道）
    <div id="4-自动化记账ocr无障碍这种稳定性太差了lsposed-hook才是王道" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-%e8%87%aa%e5%8a%a8%e5%8c%96%e8%ae%b0%e8%b4%a6ocr%e6%97%a0%e9%9a%9c%e7%a2%8d%e8%bf%99%e7%a7%8d%e7%a8%b3%e5%ae%9a%e6%80%a7%e5%a4%aa%e5%b7%ae%e4%ba%86lsposed-hook%e6%89%8d%e6%98%af%e7%8e%8b%e9%81%93" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>试过几种方案：</p>
<ul>
<li>通知栏提取：漏记、重复记严重</li>
<li>OCR识别：复杂场景识别率感人</li>
<li>无障碍服务：被系统kill是家常便饭</li>
</ul>
<p>最后还是 LSPosed hook 靠谱，直接 hook 支付结果回调：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Hook 微信支付成功回调</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">findAndHookMethod</span><span class="p">(</span><span class="s">"com.tencent.mm.plugin.wallet..."</span><span class="p">,</span><span class="w"> </span><span class="s">"onPaySuccess"</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">XC_MethodHook</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">protected</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">afterHookedMethod</span><span class="p">(</span><span class="n">MethodHookParam</span><span class="w"> </span><span class="n">param</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// 拿到金额、商户等信息直接入库</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">saveTransaction</span><span class="p">(</span><span class="n">param</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span></span></span></code></pre></div></div>
<p>准确率100%，跑在app进程里不会被杀，还能区分支付/退款/红包</p>
<p>参考项目：AutoAccounting（开源）</p>

<h2 class="relative group">5. 去广告
    <div id="5-去广告" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-%e5%8e%bb%e5%b9%bf%e5%91%8a" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>分几个层次：</p>
<p><strong>系统层面</strong>：</p>
<ul>
<li>AdAway：hosts方式</li>
<li>精简内置广告apk（System/app下一堆）</li>
</ul>
<p><strong>应用层面</strong>：</p>
<ul>
<li>LSPosed + 各种去广告模块</li>
<li>ReVanced（YouTube无广告）</li>
<li>Bilibili HD（首页清爽不少）</li>
</ul>
<p><strong>网络层面</strong>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># iptables 拦截广告域名</span>
</span></span><span class="line"><span class="cl">iptables -A OUTPUT -d ad.xxx.com -j REJECT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 或者透明代理 + AdGuard DNS</span></span></span></code></pre></div></div>
<p>激进点的可以反编译SystemUI去掉负一屏广告，重新打包刷入</p>

<h2 class="relative group">6. CPU/GPU降压，调教续航
    <div id="6-cpugpu降压调教续航" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#6-cpugpu%e9%99%8d%e5%8e%8b%e8%b0%83%e6%95%99%e7%bb%ad%e8%88%aa" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>原理：同频率降低电压 = 降低功耗和发热</p>
<p>每颗 SoC 体质不同，我的 8 Gen2 测试结果：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">原厂：大核 3.2GHz @ 1.05V, GPU 680MHz @ 0.90V
</span></span><span class="line"><span class="cl">降压：大核 3.2GHz @ 0.98V (-70mV), GPU 680MHz @ 0.85V (-50mV)
</span></span><span class="line"><span class="cl">效果：续航提升约 18%，温度降 2-3℃</span></span></code></pre></div></div>
<p>操作方式：</p>
<ul>
<li>改内核 device tree 电压表（需要重新编译）</li>
<li>Franco Kernel Manager 实时调整（方便）</li>
</ul>
<p>监控工具：CPU Float（实时频率温度）</p>

<h2 class="relative group">7. 解温控、魔改充电协议
    <div id="7-解温控魔改充电协议" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#7-%e8%a7%a3%e6%b8%a9%e6%8e%a7%e9%ad%94%e6%94%b9%e5%85%85%e7%94%b5%e5%8d%8f%e8%ae%ae" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">温控
    <div id="温控" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b8%a9%e6%8e%a7" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>厂商温控太保守，45℃就开始降频，实际芯片耐受温度在85℃以上</p>
<p>修改 <code>/sys/class/thermal/</code> 配置，提高降频阈值到 55℃左右</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 查看温控策略</span>
</span></span><span class="line"><span class="cl">cat /sys/class/thermal/thermal_zone*/temp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 修改降频阈值（需要改内核或开机脚本）</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">55000</span> > /sys/class/thermal/thermal_zone0/trip_point_0_temp</span></span></code></pre></div></div>

<h3 class="relative group">充电
    <div id="充电" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%85%e7%94%b5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>限制充电功率保护电池（长期插电的设备）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 限制充电电流 2A</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">2000000</span> > /sys/class/power_supply/battery/constant_charge_current_max
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 充到80%停止</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">80</span> > /sys/class/power_supply/battery/charge_control_limit</span></span></code></pre></div></div>
<p>实用模块：ACC (Advanced Charging Controller)，支持定时充电、智能充电等</p>

<h2 class="relative group">8. Termux 完全体
    <div id="8-termux-完全体" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#8-termux-%e5%ae%8c%e5%85%a8%e4%bd%93" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>无 root 的 Termux 限制太多：</p>
<ul>
<li>不能访问 /data, /system</li>
<li>不能绑定 80/443 端口</li>
<li>不能用 tcpdump/iptables</li>
<li>某些系统调用受限</li>
</ul>
<p>root 后解锁：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 完整 Linux 发行版</span>
</span></span><span class="line"><span class="cl">proot-distro install debian
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 抓包分析</span>
</span></span><span class="line"><span class="cl">tcpdump -i wlan0 -w capture.pcap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 跑 nginx 绑定 80 端口</span>
</span></span><span class="line"><span class="cl">nginx -c /data/nginx.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 备份分区</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/dev/block/by-name/boot <span class="nv">of</span><span class="o">=</span>/sdcard/boot.img
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 刷自编译内核</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/sdcard/custom_boot.img <span class="nv">of</span><span class="o">=</span>/dev/block/by-name/boot</span></span></code></pre></div></div>
<p>配合 Tasker 可以做很多自动化，比如：</p>
<ul>
<li>连特定WiFi时启动 SSH/Samba</li>
<li>监控电池温度自动调性能</li>
<li>定时清理缓存、备份数据</li>
</ul>
<hr>

<h2 class="relative group">总结
    <div id="总结" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%bb%e7%bb%93" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>2025年root依然是刚需，主要是：</p>
<ul>
<li>性能压榨（自定义内核）</li>
<li>隐私保护（去云控、去广告）</li>
<li>功能扩展（自动化、跨生态）</li>
<li>学习成长（系统底层知识）</li>
</ul>
<p>当然也有风险：</p>
<ul>
<li>保修失效（已过保无所谓）</li>
<li>安全风险（别乱授权）</li>
<li>OTA升级麻烦（习惯手动刷）</li>
<li>部分app检测root（Shamiko能绕过大部分）</li>
</ul>
<p>我的策略：<strong>主力机保守，备机折腾</strong></p>
<p>折腾的过程本身就是学习和乐趣所在</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">10</span><span class="o">-</span><span class="mi">04</span><span class="nx">T14</span><span class="o">:</span><span class="mi">42</span><span class="o">:</span><span class="mi">47</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">02</span><span class="nx">T06</span><span class="o">:</span><span class="mi">59</span><span class="o">:</span><span class="mi">10</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/90
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>一个简单的OpenWrt procd 守护脚本</title>
      <link>https://blog.ferstar.org/posts/openwrt-procd-daemon-script/</link>
      <pubDate>Mon, 05 May 2025 06:38:18 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/openwrt-procd-daemon-script/</guid>
      <description>回老家把家里的红米 AX5固件升级了一下，因为之前用 ShellCrash 固定过 ssh，所以升完以后 ssh 包括 ShellCrash 依然存在，但是之前随手写的 tuic init 脚本因为在/etc/init.d目录，被覆盖掉，需要重写，在这里备忘一下。&#xA;</description><content:encoded><![CDATA[<p>回老家把家里的红米 AX5固件升级了一下，因为之前用 ShellCrash 固定过 ssh，所以升完以后 ssh 包括 ShellCrash 依然存在，但是之前随手写的 tuic init 脚本因为在<code>/etc/init.d</code>目录，被覆盖掉，需要重写，在这里备忘一下。</p>

<h3 class="relative group">procd 守护逻辑
    <div id="procd-守护逻辑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#procd-%e5%ae%88%e6%8a%a4%e9%80%bb%e8%be%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>OpenWrt 的 <code>procd</code> 系统通过以下逻辑管理服务生命周期，重点在于 <code>respawn</code> 机制：</p>
<pre class="not-prose mermaid">stateDiagram-v2
    [*] --> Start
    Start --> Instance_Init: procd_open_instance
    Instance_Init --> Running: procd_close_instance
    Running --> Crash: Process Exit
    Crash --> Respawn: Check respawn param
    Respawn --> Running: Restart Process
    Running --> Stop: /etc/init.d/tuic stop
    Stop --> [*]</pre>

<h3 class="relative group">脚本示例
    <div id="脚本示例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%84%9a%e6%9c%ac%e7%a4%ba%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>将以下内容保存为 <code>/etc/init.d/tuic</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/bin/sh /etc/rc.common
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">START</span><span class="o">=</span><span class="m">99</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SERVICE_DAEMONIZE</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="nv">SERVICE_WRITE_PID</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="nv">USE_PROCD</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="nv">COMMAND</span><span class="o">=</span><span class="s2">"/etc/clash/tools/tuic -c /etc/clash/config.json"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">start_service<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">        procd_open_instance
</span></span><span class="line"><span class="cl">        procd_set_param user root
</span></span><span class="line"><span class="cl">        procd_set_param respawn
</span></span><span class="line"><span class="cl">        procd_set_param <span class="nb">command</span> <span class="nv">$COMMAND</span>
</span></span><span class="line"><span class="cl">        procd_set_param stderr <span class="m">0</span>
</span></span><span class="line"><span class="cl">        procd_set_param stdout <span class="m">0</span>
</span></span><span class="line"><span class="cl">        procd_close_instance
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">stop_service<span class="o">(){</span>
</span></span><span class="line"><span class="cl">        procd_close_instance
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>

<h3 class="relative group">使用方法
    <div id="使用方法" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bd%bf%e7%94%a8%e6%96%b9%e6%b3%95" aria-label="锚点">#</a>
    </span>
    
</h3>
<ol>
<li><strong>赋权</strong>：<code>chmod +x /etc/init.d/tuic</code></li>
<li><strong>常用命令</strong>：
<ul>
<li><code>start</code> / <code>stop</code> / <code>restart</code>: 管理服务当前运行状态。</li>
<li><code>enable</code> / <code>disable</code>: 开启/关闭开机自启（符号链接管理）。</li>
<li><code>reload</code>: 重新加载配置。</li>
</ul>
</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">root@XiaoQiang:~# /etc/init.d/tuic
</span></span><span class="line"><span class="cl">Syntax: /etc/init.d/tuic <span class="o">[</span>command<span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Available commands:
</span></span><span class="line"><span class="cl">        start   Start the service
</span></span><span class="line"><span class="cl">        stop    Stop the service
</span></span><span class="line"><span class="cl">        restart Restart the service
</span></span><span class="line"><span class="cl">        reload  Reload configuration files <span class="o">(</span>or restart <span class="k">if</span> service does not implement reload<span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">enable</span>  Enable service autostart
</span></span><span class="line"><span class="cl">        disable Disable service autostart</span></span></code></pre></div></div>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">05</span><span class="o">-</span><span class="mi">05</span><span class="nx">T06</span><span class="o">:</span><span class="mi">38</span><span class="o">:</span><span class="mi">18</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">04</span><span class="nx">T06</span><span class="o">:</span><span class="mi">10</span><span class="o">:</span><span class="mi">00</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">comment</span><span class="err">@</span><span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/89
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How to Build Kernel for Realme GT5 Pro(RMX3888)</title>
      <link>https://blog.ferstar.org/posts/build-kernel-realme-gt5-pro/</link>
      <pubDate>Sat, 25 Jan 2025 10:45:41 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/build-kernel-realme-gt5-pro/</guid>
      <description>真我 Realme GT5 Pro (RMX3888) 内核编译实操指南。提供一键编译脚本、Bazel 编译优化配置及 AnyKernel3 打包方法，解决 WiFi、蓝牙驱动兼容性坑位。</description><content:encoded><![CDATA[<p>好了，懒人脚本：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone git@github.com:ferstar/kernel_manifest.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> kernel_manifest
</span></span><span class="line"><span class="cl">chmod +x build.sh
</span></span><span class="line"><span class="cl">./build.sh</span></span></code></pre></div></div>
<hr>
<ol>
<li>环境&配置</li>
</ol>
<p>操作系统 Ubuntu、Debian 都可以，内存 16 GB 起步，不够的自行加 swap，硬盘空间 50GB</p>
<p>装依赖：<code>sudo apt install git gnupg flex bison build-essential zip curl zlib1g-dev unzip rsync python3(如果还有缺的自己补)</code></p>
<ol start="2">
<li>装 repo 工具</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo > /usr/bin/repo
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/bin/repo</span></span></code></pre></div></div>
<ol start="3">
<li>拉取机型源码&编译</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir kernel
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> kernel
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">REPO_URL</span><span class="o">=</span><span class="s1">'https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'</span> 
</span></span><span class="line"><span class="cl">repo init --repo-rev<span class="o">=</span>v2.16 --depth<span class="o">=</span><span class="m">1</span> -u https://github.com/ferstar/kernel_manifest.git -b realme/sm8650 -m gt5pro_v.xml
</span></span><span class="line"><span class="cl">repo sync --no-tags
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> kernel_platform
</span></span><span class="line"><span class="cl"><span class="c1"># 这个必须删，不然内核启动后没法驱动WiFi、蓝牙和4/5G</span>
</span></span><span class="line"><span class="cl">rm common/android/abi_gki_protected_exports_*
</span></span><span class="line"><span class="cl"><span class="c1"># 编译</span>
</span></span><span class="line"><span class="cl">python build_with_bazel.py -t pineapple gki  --lto<span class="o">=</span>thin --config<span class="o">=</span>fast --disk_cache<span class="o">=</span><span class="nv">$HOME</span>/.cache/bazel --//msm-kernel:skip_abi<span class="o">=</span><span class="nb">true</span> --//msm-kernel:skip_abl<span class="o">=</span><span class="nb">true</span> -o <span class="s2">"</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/out"</span> <span class="o">||</span> true</span></span></code></pre></div></div>
<p>因为真我只开源了Kernel和Vendor，没有common source，这个方案是跟一加Ace5杂交的，所以上面的编译最终会有报错，但不影响内核的正常生成：</p>
<p><code>bazel-out/k8-fastbuild/bin/msm-kernel/pineapple_gki_kbuild_mixed_tree/Image</code></p>
<p>将其塞进<code>AnyKernel3</code>的压缩包内即可刷入手机。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">25</span><span class="nx">T10</span><span class="o">:</span><span class="mi">45</span><span class="o">:</span><span class="mi">41</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">27</span><span class="nx">T16</span><span class="o">:</span><span class="mi">26</span><span class="o">:</span><span class="mi">43</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/88
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How to Build Kernel for Redmi Note11 Turbo Pro(xaga) </title>
      <link>https://blog.ferstar.org/posts/build-kernel-redmi-note11-pro/</link>
      <pubDate>Mon, 13 Jan 2025 07:45:39 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/build-kernel-redmi-note11-pro/</guid>
      <description>很简单，直接看：https://github.com/ferstar/xiaomi_xaga_kernel 中的 buildebug.sh 脚本即可，把该装的依赖装好，完事。&#xA;</description><content:encoded><![CDATA[<p>很简单，直接看：https://github.com/ferstar/xiaomi_xaga_kernel 中的 buildebug.sh 脚本即可，把该装的依赖装好，完事。</p>
<p>也可以直接用我编译好的内核：</p>
<p>下载： <a href="https://ferstar.lanzoum.com/b0ziwk67e"  target="_blank" rel="noreferrer">https://ferstar.lanzoum.com/b0ziwk67e</a>
密码：g1mx</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">13</span><span class="nx">T07</span><span class="o">:</span><span class="mi">45</span><span class="o">:</span><span class="mi">39</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">25</span><span class="nx">T10</span><span class="o">:</span><span class="mi">21</span><span class="o">:</span><span class="mi">24</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/86
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>修复联想笔记本Linux下合盖睡死与功能键异常关机问题</title>
      <link>https://blog.ferstar.org/posts/fix-lenovo-linux-suspend-sleep-bug/</link>
      <pubDate>Mon, 13 Jan 2025 07:15:03 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/fix-lenovo-linux-suspend-sleep-bug/</guid>
      <description>问题背景 # 许多使用联想ThinkBook 2024系列笔记本的Linux用户报告了两个典型问题：&#xA;</description><content:encoded><![CDATA[
<h2 class="relative group">问题背景
    <div id="问题背景" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>许多使用联想ThinkBook 2024系列笔记本的Linux用户报告了两个典型问题：</p>
<ol>
<li>
<p><strong>合盖睡死问题</strong><br>
当合上笔记本盖时，设备会直接断电关机而非进入挂起状态，导致工作状态丢失</p>
</li>
<li>
<p><strong>功能键异常关机</strong><br>
使用<code>Fn+F5</code>/<code>Fn+F6</code>组合键时（非高频次使用），可能触发意外关机</p>
</li>
</ol>
<p>该问题在Ubuntu 24.04（内核6.9+）和Arch Linux（内核6.10+）等多个发行版中复现，经排查与ACPI电源管理模块的兼容性有关。</p>
<hr>

<h2 class="relative group">解决方案
    <div id="解决方案" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>开源社区开发的<code>ideapad-laptop-tb-dkms</code>内核模块通过以下方式解决问题：</p>
<ul>
<li>重写ACPI事件处理逻辑</li>
<li>修正电源状态转换机制</li>
<li>禁用异常的功能键信号</li>
</ul>

<h3 class="relative group">兼容设备
    <div id="兼容设备" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%bc%e5%ae%b9%e8%ae%be%e5%a4%87" aria-label="锚点">#</a>
    </span>
    
</h3>
<ul>
<li>ThinkBook 2024 16+ IMH</li>
<li>ThinkBook 2024 14 G6+ AHP</li>
<li>ThinkBook 16 G6+ AHP</li>
</ul>
<hr>

<h2 class="relative group">安装指南
    <div id="安装指南" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e6%8c%87%e5%8d%97" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">对于Arch系发行版
    <div id="对于arch系发行版" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%af%b9%e4%ba%8earch%e7%b3%bb%e5%8f%91%e8%a1%8c%e7%89%88" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S ideapad-laptop-tb-dkms</span></span></code></pre></div></div>

<h3 class="relative group">通用安装方式（支持Ubuntu/Debian/Fedora等）
    <div id="通用安装方式支持ubuntudebianfedora等" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%80%9a%e7%94%a8%e5%ae%89%e8%a3%85%e6%96%b9%e5%bc%8f%e6%94%af%e6%8c%81ubuntudebianfedora%e7%ad%89" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 编译并安装DKMS模块</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/ferstar/ideapad-laptop-tb.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ideapad-laptop-tb-dkms
</span></span><span class="line"><span class="cl">sudo dkms add .
</span></span><span class="line"><span class="cl">sudo dkms install ideapad-laptop-tb/6.10  <span class="c1"># 版本号需匹配内核版本</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 禁用原生冲突模块</span>
</span></span><span class="line"><span class="cl">sudo cp dkms/blacklist-ideapad-laptop-tb-dkms.conf /etc/modprobe.d/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 重启生效</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div>

<h3 class="relative group">卸载方法
    <div id="卸载方法" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8d%b8%e8%bd%bd%e6%96%b9%e6%b3%95" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dkms remove ideapad-laptop-tb/6.10 --all
</span></span><span class="line"><span class="cl">sudo rm /etc/modprobe.d/blacklist-ideapad-laptop-tb-dkms.conf
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">注意事项
    <div id="注意事项" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b3%a8%e6%84%8f%e4%ba%8b%e9%a1%b9" aria-label="锚点">#</a>
    </span>
    
</h2>
<ol>
<li>
<p><strong>禁麦/音LED灯不亮</strong>：移步这个项目 <a href="https://github.com/ferstar/lenovo-wmi-hotkey-utilities"  target="_blank" rel="noreferrer">lenovo-wmi-hotkey-utilities</a>，如果你的机型键盘按键并没有这两个LED灯，那么可以忽略</p>
</li>
<li>
<p><strong>电源状态验证</strong><br>
安装后可通过以下命令测试：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl <span class="nb">suspend</span>  <span class="c1"># 测试挂起功能</span>
</span></span><span class="line"><span class="cl">lidctrl close      <span class="c1"># 测试合盖响应</span></span></span></code></pre></div></div>
</li>
</ol>
<hr>
<ol>
<li><a href="https://bbs.archlinuxcn.org/viewtopic.php?id=14053"  target="_blank" rel="noreferrer">https://bbs.archlinuxcn.org/viewtopic.php?id=14053</a></li>
<li><a href="https://github.com/ferstar/ideapad-laptop-tb"  target="_blank" rel="noreferrer">https://github.com/ferstar/ideapad-laptop-tb</a></li>
</ol>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">13</span><span class="nx">T07</span><span class="o">:</span><span class="mi">15</span><span class="o">:</span><span class="mi">03</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">05</span><span class="o">-</span><span class="mi">24</span><span class="nx">T03</span><span class="o">:</span><span class="mi">21</span><span class="o">:</span><span class="mi">49</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/85
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How to Fix High CPU load of kwin_x11 when locking or turning off the screen</title>
      <link>https://blog.ferstar.org/posts/fix-kwin-x11-high-cpu/</link>
      <pubDate>Mon, 13 Jan 2025 02:01:49 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/fix-kwin-x11-high-cpu/</guid>
      <description>TL;DR, use xsecurelock instead of the KDE default screenlocker&#xA;好像也没什么好说的，这是一个持续了一年的 bug 至今没有修复：https://bugs.kde.org/show_bug.cgi?id=484323&#xA;</description><content:encoded><![CDATA[<blockquote><p>TL;DR, use <a href="https://github.com/google/xsecurelock"  target="_blank" rel="noreferrer">xsecurelock</a> instead of the KDE default screenlocker</p>
</blockquote><p>好像也没什么好说的，这是一个持续了一年的 bug 至今没有修复：https://bugs.kde.org/show_bug.cgi?id=484323</p>
<p>所以只能曲线救国，换一种锁屏方式，即 xsecurelock。只要把 KDE 自带的 锁屏关闭，然后把锁屏快捷键 Meta + L 绑定给 xsecurelock 程序即可。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">13</span><span class="nx">T02</span><span class="o">:</span><span class="mi">01</span><span class="o">:</span><span class="mi">49</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">25</span><span class="nx">T10</span><span class="o">:</span><span class="mi">03</span><span class="o">:</span><span class="mi">22</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/84
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How to update InsydeH2O BIOS with pure Linux</title>
      <link>https://blog.ferstar.org/posts/update-insydeh2o-bios-linux/</link>
      <pubDate>Sun, 12 Jan 2025 16:58:10 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/update-insydeh2o-bios-linux/</guid>
      <description>BIOS 更新离不开 Windows、还得留大分区；用纯 Linux 解包与刷写流程替代 Windows 工具；无需保留 Windows 也能稳定升级。</description><content:encoded><![CDATA[<p>作为一个骨灰级 Linux 发烧友，我的电脑到手基本都是第一时间换装 Linux 发行版。这样设备厂商出厂自带的 Windows 系统几乎毫无作用，但我又不能把它干掉——我还得靠它来更新厂商发布的 BIOS。为了偶尔更 BIOS 的需求而不得不保留这近 100GB 磁盘占用的 Windows 着实有些蛋疼，当然我知道可以用类似 WIN2GO 的玩意来更 BIOS，但这又有什么区别呢，还是离不开 Windows。</p>
<p>终于赶在 2024 年的尾巴，我在 Twitter 上闲逛时发现有人提到了纯 Linux 环境更新 BIOS 的方案：https://x.com/felixonmars/status/1876646199207604351</p>
<p>虽然寥寥数语，但激起了心中折腾的一团火：有戏&干之！</p>
<p>我手头笔记本清一色都是联想的 ThinkBook，先拿家里的机器 A 开刀，配置如下：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">OS: Ubuntu oracular 24.10 x86_64
</span></span><span class="line"><span class="cl">Host: 21D0 <span class="o">(</span>ThinkBook <span class="m">14</span> G4+ ARA<span class="o">)</span>
</span></span><span class="line"><span class="cl">Kernel: Linux 6.12.9-bigv
</span></span><span class="line"><span class="cl">Uptime: <span class="m">1</span> day, <span class="m">53</span> mins
</span></span><span class="line"><span class="cl">Packages: <span class="m">2800</span> <span class="o">(</span>dpkg<span class="o">)</span>, <span class="m">7</span> <span class="o">(</span>flatpak<span class="o">)</span>
</span></span><span class="line"><span class="cl">Shell: zsh 5.9
</span></span><span class="line"><span class="cl">Display <span class="o">(</span>AUOC391<span class="o">)</span>: 2880x1800 @ <span class="m">90</span> Hz in 14<span class="s2">"
</span></span></span><span class="line"><span class="cl"><span class="s2">DE: GNOME
</span></span></span><span class="line"><span class="cl"><span class="s2">WM: Mutter (X11)
</span></span></span><span class="line"><span class="cl"><span class="s2">WM Theme: Yaru
</span></span></span><span class="line"><span class="cl"><span class="s2">Theme: Yaru [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Icons: Yaru [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Font: Cantarell (11pt) [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Cursor: DMZ-White (48px)
</span></span></span><span class="line"><span class="cl"><span class="s2">Terminal: tmux 3.4
</span></span></span><span class="line"><span class="cl"><span class="s2">CPU: AMD Ryzen 7 6800H (16) @ 4.79 GHz
</span></span></span><span class="line"><span class="cl"><span class="s2">GPU: AMD Radeon 680M [Integrated]
</span></span></span><span class="line"><span class="cl"><span class="s2">Memory: 12.95 GiB / 29.55 GiB (44%)
</span></span></span><span class="line"><span class="cl"><span class="s2">Swap: 5.25 MiB / 32.00 GiB (0%)
</span></span></span><span class="line"><span class="cl"><span class="s2">Disk (/): 312.72 GiB / 414.32 GiB (75%) - btrfs
</span></span></span><span class="line"><span class="cl"><span class="s2">Local IP (wlan0): 192.168.31.157/24
</span></span></span><span class="line"><span class="cl"><span class="s2">Battery (AP16L5J): 77% [AC Connected]
</span></span></span><span class="line"><span class="cl"><span class="s2">Locale: en_US.UTF-8</span></span></span></code></pre></div></div>
<ol>
<li>下载 Framework 家的 UEFI shell 更新工具，我们只用到<code>H2OFFT-Sx64.efi</code>，把他保存至<code>/boot/efi</code>：https://downloads.frame.work/bios/Framework_Laptop_13_13th_Gen_Intel_Core_BIOS__3.05_EFI.zip</li>
<li>下载 UEFI-Shell，将<code>shellx64.efi</code>保存至<code>/boot/efi</code>：https://github.com/pbatard/UEFI-Shell/releases/tag/24H2</li>
<li>去联想驱动官网下载你笔记本型号对应的 BIOS 更新程序，通常会是<code>j6cn50ww.exe</code>的名字</li>
<li>尝试用<code>7z</code>解压这个<code>exe</code>文件：<code>7z e j6cn50ww.exe</code>，可能有类似的输出：<code>Comments: This installation was built with Inno Setup.</code></li>
<li>我们需要<code>innoextract</code>来解包：<code>innoextract -e j6cn50ww.exe</code>，你会得到一个新的<code>exe</code>，我这里的名字是<code>J6CN50WW.exe</code></li>
<li>再用<code>7z</code>解压：<code>7z e J6CN50WW.exe</code>，这次我们应该可以顺利得到真实的 BIOS 固件<code>WinJ6CN50WW.fd</code></li>
<li>将<code>WinJ6CN50WW.fd</code>保存至<code>/boot/efi</code></li>
<li>添加 UEFI-Shell 启动项，编辑<code>/etc/grub.d/40_custom</code>，填入如下内容后执行<code>sudo update-grub</code>：
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">menuentry <span class="s2">"UEFI Shell"</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    insmod part_gpt
</span></span><span class="line"><span class="cl">    insmod chain
</span></span><span class="line"><span class="cl">    <span class="nb">set</span> <span class="nv">root</span><span class="o">=</span><span class="s1">'(hd0,gpt1)'</span>
</span></span><span class="line"><span class="cl">    chainloader /shellx64.efi
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>
</li>
<li>重启进入 UEFI-Shell，输入：<code>FS0:</code> 回车进入 EFI 分区，再输入：<code>H2OFFT-Sx64.efi WinJ6CN50WW.fd</code>回车</li>
<li>此时熟悉的 BIOS 更新界面出现，耐心等待进度条走完即可完成更新</li>
</ol>
<p>终于可以把自带的 Windows 分区扔进垃圾堆了✌️</p>
<hr>
<p>注意：对于某些比较新的联想机型，可能<code>7z</code>就能直接解压得到 BIOS 固件了，一般是 <code>xxx.bin</code> 的命名，<code>innoextract</code> 并不是必须的；还有<code>7z</code>也不不是必须的，你可以换任何能解包<code>exe</code>的工具，比如<code>bsdtar</code>之类。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">12</span><span class="nx">T16</span><span class="o">:</span><span class="mi">58</span><span class="o">:</span><span class="mi">10</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">12</span><span class="nx">T17</span><span class="o">:</span><span class="mi">04</span><span class="o">:</span><span class="mi">21</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/83
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Build my own patched Ubuntu mainline kernel</title>
      <link>https://blog.ferstar.org/posts/build-ubuntu-mainline-kernel/</link>
      <pubDate>Sun, 12 Jan 2025 16:16:49 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/build-ubuntu-mainline-kernel/</guid>
      <description>记录下用 ubuntu-mainline-kernel.sh 编译打补丁内核的过程&#xA;背景 # Ubuntu 官方内核太保守，想用 mainline 最新内核，但又需要打自己的补丁（比如硬件驱动、性能优化之类的）&#xA;</description><content:encoded><![CDATA[<p>记录下用 ubuntu-mainline-kernel.sh 编译打补丁内核的过程</p>

<h2 class="relative group">背景
    <div id="背景" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%83%8c%e6%99%af" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>Ubuntu 官方内核太保守，想用 mainline 最新内核，但又需要打自己的补丁（比如硬件驱动、性能优化之类的）</p>
<p>找到个不错的工具：<a href="https://github.com/pimlie/ubuntu-mainline-kernel.sh"  target="_blank" rel="noreferrer">ubuntu-mainline-kernel.sh</a></p>

<h2 class="relative group">工具介绍
    <div id="工具介绍" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b7%a5%e5%85%b7%e4%bb%8b%e7%bb%8d" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这个脚本可以：</p>
<ul>
<li>从 Ubuntu Kernel PPA 直接下载安装 mainline 内核</li>
<li>本地编译内核（实验性功能）</li>
<li>支持 SecureBoot 签名</li>
<li>自动检查新版本</li>
</ul>
<p>⚠️ <strong>警告</strong>：mainline 内核不受官方支持，生产环境慎用，记得保留默认内核做备份</p>

<h2 class="relative group">安装脚本
    <div id="安装脚本" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e8%84%9a%e6%9c%ac" aria-label="锚点">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 安装依赖</span>
</span></span><span class="line"><span class="cl">sudo apt install wget
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 下载脚本</span>
</span></span><span class="line"><span class="cl">wget https://raw.githubusercontent.com/pimlie/ubuntu-mainline-kernel.sh/master/ubuntu-mainline-kernel.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 赋予执行权限</span>
</span></span><span class="line"><span class="cl">chmod +x ubuntu-mainline-kernel.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 移动到系统路径</span>
</span></span><span class="line"><span class="cl">sudo mv ubuntu-mainline-kernel.sh /usr/local/bin/</span></span></code></pre></div></div>

<h2 class="relative group">常用命令
    <div id="常用命令" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e7%94%a8%e5%91%bd%e4%bb%a4" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">查看可用版本
    <div id="查看可用版本" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9f%a5%e7%9c%8b%e5%8f%af%e7%94%a8%e7%89%88%e6%9c%ac" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 列出所有远程版本</span>
</span></span><span class="line"><span class="cl">ubuntu-mainline-kernel.sh -r
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 包括 RC 版本</span>
</span></span><span class="line"><span class="cl">ubuntu-mainline-kernel.sh -r --rc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 搜索特定版本</span>
</span></span><span class="line"><span class="cl">ubuntu-mainline-kernel.sh -r 6.8</span></span></code></pre></div></div>

<h3 class="relative group">安装预编译内核
    <div id="安装预编译内核" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e9%a2%84%e7%bc%96%e8%af%91%e5%86%85%e6%a0%b8" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 安装指定版本</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -i 6.8.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 包括 RC 版本</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -i 6.8-rc5 --rc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 安装 low-latency 版本（低延迟优化）</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -i 6.8.0 --low-latency</span></span></code></pre></div></div>

<h3 class="relative group">卸载内核
    <div id="卸载内核" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8d%b8%e8%bd%bd%e5%86%85%e6%a0%b8" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 列出已安装的内核</span>
</span></span><span class="line"><span class="cl">ubuntu-mainline-kernel.sh -l
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 卸载指定版本</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -u 6.8.0</span></span></code></pre></div></div>

<h3 class="relative group">本地编译（打补丁用这个）
    <div id="本地编译打补丁用这个" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%ac%e5%9c%b0%e7%bc%96%e8%af%91%e6%89%93%e8%a1%a5%e4%b8%81%e7%94%a8%e8%bf%99%e4%b8%aa" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 本地编译并安装</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -b 6.8.0</span></span></code></pre></div></div>

<h2 class="relative group">本地编译打补丁流程
    <div id="本地编译打补丁流程" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%ac%e5%9c%b0%e7%bc%96%e8%af%91%e6%89%93%e8%a1%a5%e4%b8%81%e6%b5%81%e7%a8%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这是重点，可以在编译前打自己的补丁</p>

<h3 class="relative group">准备工作
    <div id="准备工作" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 安装依赖</span>
</span></span><span class="line"><span class="cl">sudo apt install git docker.io
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 把自己加到 docker 组（避免每次 sudo）</span>
</span></span><span class="line"><span class="cl">sudo usermod -aG docker <span class="nv">$USER</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 需要重新登录生效</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 预留空间：源码约 3GB，编译过程可能 10GB+</span>
</span></span><span class="line"><span class="cl">df -h /var/lib/docker</span></span></code></pre></div></div>

<h3 class="relative group">获取内核源码
    <div id="获取内核源码" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%8e%b7%e5%8f%96%e5%86%85%e6%a0%b8%e6%ba%90%e7%a0%81" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>脚本使用 Docker 编译，基于 TuxInvader 的 focal-mainline-builder 镜像</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 克隆源码（脚本会自动做，这里是手动方式）</span>
</span></span><span class="line"><span class="cl">git clone --depth<span class="o">=</span><span class="m">1</span> --branch v6.8 <span class="se">\
</span></span></span><span class="line"><span class="cl">  git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git <span class="se">\
</span></span></span><span class="line"><span class="cl">  linux-6.8
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> linux-6.8</span></span></code></pre></div></div>

<h3 class="relative group">打补丁
    <div id="打补丁" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%89%93%e8%a1%a5%e4%b8%81" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>把自定义补丁放到源码目录：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 示例：打个驱动补丁</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> linux-6.8
</span></span><span class="line"><span class="cl">patch -p1 < ~/my-driver.patch
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 或者直接修改源码文件</span>
</span></span><span class="line"><span class="cl">vim drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c</span></span></code></pre></div></div>

<h3 class="relative group">修改配置（可选）
    <div id="修改配置可选" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bf%ae%e6%94%b9%e9%85%8d%e7%bd%ae%e5%8f%af%e9%80%89" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 复制当前系统内核配置</span>
</span></span><span class="line"><span class="cl">cp /boot/config-<span class="k">$(</span>uname -r<span class="k">)</span> .config
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 更新配置（使用默认值）</span>
</span></span><span class="line"><span class="cl">make olddefconfig
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 或者手动配置</span>
</span></span><span class="line"><span class="cl">make menuconfig</span></span></code></pre></div></div>

<h3 class="relative group">开始编译
    <div id="开始编译" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bc%80%e5%a7%8b%e7%bc%96%e8%af%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>使用脚本编译：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># -b 参数会自动拉取 Docker 镜像并编译</span>
</span></span><span class="line"><span class="cl">sudo ubuntu-mainline-kernel.sh -b 6.8.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 编译完成后会自动安装</span></span></span></code></pre></div></div>
<p><strong>编译参数调整</strong>（修改脚本或手动 Docker 编译）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 手动 Docker 编译方式</span>
</span></span><span class="line"><span class="cl">docker run --rm -it <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v <span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span>:/build <span class="se">\
</span></span></span><span class="line"><span class="cl">  tuxinvader/focal-mainline-builder:latest <span class="se">\
</span></span></span><span class="line"><span class="cl">  /bin/bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 在容器内</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /build
</span></span><span class="line"><span class="cl">make -j<span class="k">$(</span>nproc<span class="k">)</span> bindeb-pkg <span class="nv">LOCALVERSION</span><span class="o">=</span>-custom</span></span></code></pre></div></div>

<h3 class="relative group">安装编译好的内核
    <div id="安装编译好的内核" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e7%bc%96%e8%af%91%e5%a5%bd%e7%9a%84%e5%86%85%e6%a0%b8" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 如果是手动编译，会在上层目录生成 .deb 文件</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">ls -lh *.deb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 安装</span>
</span></span><span class="line"><span class="cl">sudo dpkg -i linux-*.deb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 更新 GRUB</span>
</span></span><span class="line"><span class="cl">sudo update-grub</span></span></code></pre></div></div>

<h2 class="relative group">SecureBoot 签名（需要的话）
    <div id="secureboot-签名需要的话" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#secureboot-%e7%ad%be%e5%90%8d%e9%9c%80%e8%a6%81%e7%9a%84%e8%af%9d" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>如果启用了 SecureBoot，需要签名内核</p>

<h3 class="relative group">生成签名密钥
    <div id="生成签名密钥" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%9f%e6%88%90%e7%ad%be%e5%90%8d%e5%af%86%e9%92%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 创建密钥目录</span>
</span></span><span class="line"><span class="cl">mkdir -p ~/sb-keys
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/sb-keys
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 生成 MOK 密钥对</span>
</span></span><span class="line"><span class="cl">openssl req -new -x509 -newkey rsa:2048 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -keyout MOK.priv <span class="se">\
</span></span></span><span class="line"><span class="cl">  -outform DER <span class="se">\
</span></span></span><span class="line"><span class="cl">  -out MOK.der <span class="se">\
</span></span></span><span class="line"><span class="cl">  -days <span class="m">36500</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -subj <span class="s2">"/CN=My Kernel Signing Key/"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -nodes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 注册 MOK 密钥（重启后会提示输入密码）</span>
</span></span><span class="line"><span class="cl">sudo mokutil --import MOK.der
</span></span><span class="line"><span class="cl"><span class="c1"># 设置一个临时密码，重启时输入</span></span></span></code></pre></div></div>

<h3 class="relative group">签名内核
    <div id="签名内核" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ad%be%e5%90%8d%e5%86%85%e6%a0%b8" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 签名内核和模块</span>
</span></span><span class="line"><span class="cl">sudo sbsign --key MOK.priv --cert MOK.der <span class="se">\
</span></span></span><span class="line"><span class="cl">  /boot/vmlinuz-6.8.0-custom
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 签名模块（如果需要）</span>
</span></span><span class="line"><span class="cl">sudo find /lib/modules/6.8.0-custom -name <span class="s2">"*.ko"</span> -exec <span class="se">\
</span></span></span><span class="line"><span class="cl">  sbsign --key MOK.priv --cert MOK.der <span class="o">{}</span> <span class="se">\;</span></span></span></code></pre></div></div>
<p>重启选择签名后的内核启动</p>

<h2 class="relative group">常见问题
    <div id="常见问题" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">编译失败
    <div id="编译失败" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bc%96%e8%af%91%e5%a4%b1%e8%b4%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Docker 镜像可能过期，尝试：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 拉取最新镜像</span>
</span></span><span class="line"><span class="cl">docker pull tuxinvader/focal-mainline-builder:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 或者手动编译不用 Docker</span>
</span></span><span class="line"><span class="cl">sudo apt install build-essential libncurses-dev <span class="se">\
</span></span></span><span class="line"><span class="cl">  bison flex libssl-dev libelf-dev
</span></span><span class="line"><span class="cl">make -j<span class="k">$(</span>nproc<span class="k">)</span> bindeb-pkg</span></span></code></pre></div></div>

<h3 class="relative group">缺少固件
    <div id="缺少固件" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bc%ba%e5%b0%91%e5%9b%ba%e4%bb%b6" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>某些驱动需要额外固件：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install linux-firmware</span></span></code></pre></div></div>

<h3 class="relative group">GRUB 不显示新内核
    <div id="grub-不显示新内核" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#grub-%e4%b8%8d%e6%98%be%e7%a4%ba%e6%96%b0%e5%86%85%e6%a0%b8" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 更新 GRUB</span>
</span></span><span class="line"><span class="cl">sudo update-grub
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 检查是否生成</span>
</span></span><span class="line"><span class="cl">grep menuentry /boot/grub/grub.cfg</span></span></code></pre></div></div>

<h2 class="relative group">参考资料
    <div id="参考资料" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li><a href="https://github.com/pimlie/ubuntu-mainline-kernel.sh"  target="_blank" rel="noreferrer">ubuntu-mainline-kernel.sh GitHub</a></li>
<li><a href="https://kernel.ubuntu.com/~kernel-ppa/mainline/"  target="_blank" rel="noreferrer">Ubuntu Kernel PPA</a></li>
<li><a href="https://github.com/TuxInvader/focal-mainline-builder"  target="_blank" rel="noreferrer">TuxInvader 的 Docker 镜像</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/"  target="_blank" rel="noreferrer">内核编译官方文档</a></li>
</ul>
<p>文档已完成</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">12</span><span class="nx">T16</span><span class="o">:</span><span class="mi">16</span><span class="o">:</span><span class="mi">49</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">02</span><span class="nx">T07</span><span class="o">:</span><span class="mi">01</span><span class="o">:</span><span class="mi">28</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/82
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>AI codereview实践</title>
      <link>https://blog.ferstar.org/posts/ai-assisted-code-review-automation/</link>
      <pubDate>Mon, 06 Jan 2025 14:55:44 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ai-assisted-code-review-automation/</guid>
      <description>最近在公司内网 CI 工作流里加入了 AI Codereview 的功能，效果确实不错，能发现一些常规 Review 很难发现的问题。废话不多说直接上步骤&amp;amp;代码。&#xA;按 author 拆分 commit，获取 diff codes P_SKIP_CI = re.compile(r&amp;#34;\[(skip\s*ci|ci\s*skip|no\s*ci)]&amp;#34;, re.IGNORECASE) @dataclass class CommitCache: path: Path = Path(tempfile.gettempdir()) / &amp;#34;commits_cache.txt&amp;#34; max_size: int = 100 ids: list[str] = field(default_factory=list) def __post_init__(self): if self.path.exists(): self.ids = [i.strip() for i in self.path.read_text().split(&amp;#34;\n&amp;#34;) if i.strip()] def add(self, *ids): self.ids = (self.ids + [i for i in ids if i not in self.ids])[-self.max_size :] self.path.write_text(&amp;#34;\n&amp;#34;.join(self.ids)) @dataclass class CommitDiff: rev: str author: str content: str parent_rev: str = field(init=False) def __post_init__(self): self.parent_rev = run(f&amp;#34;git rev-parse {self.rev}^&amp;#34;, hide=True).stdout.strip() @property def changes(self): return f&amp;#34;&amp;#34;&amp;#34;``` {run(f&amp;#34;git diff --name-only {self.rev}^!&amp;#34;, hide=True).stdout.strip()} ```&amp;#34;&amp;#34;&amp;#34; @property def msg(self): return run(f&amp;#34;git log {self.rev}^! --format=%B&amp;#34;, hide=True).stdout.strip() @property def need_review(self): return any( i.startswith(&amp;#34;diff --git&amp;#34;) and i.endswith((&amp;#34;.py&amp;#34;, &amp;#34;.sh&amp;#34;)) for i in self.content.split(&amp;#34;\n&amp;#34;) ) and not P_SKIP_CI.search(self.msg) def prepare_commit_diff(log_range: int = 10, commit_range: int = 3) -&amp;gt; Iterable[CommitDiff]: &amp;#34;&amp;#34;&amp;#34;从指定(默认为最近10次)的提交中提取各 author 最近(默认为最近3次)的修改内容&amp;#34;&amp;#34;&amp;#34; result = run(f&amp;#39;git log -n {log_range} --pretty=format:&amp;#34;%an&amp;#34; | sort | uniq&amp;#39;, hide=True) commit_cache = CommitCache() for author in result.stdout.split(&amp;#34;\n&amp;#34;): if not author: continue result = run(f&amp;#34;&amp;#34;&amp;#34;git log --author=&amp;#34;{author}&amp;#34; --pretty=format:&amp;#39;%H&amp;#39; -n {commit_range}&amp;#34;&amp;#34;&amp;#34;, hide=True).stdout for commit_id in (i for i in result.split(&amp;#34;\n&amp;#34;) if i): if commit_id not in commit_cache.ids: # 避免重复检查 commit_cache.add(commit_id) diff = CommitDiff( author=author, rev=commit_id, content=run( f&amp;#34;git diff --ignore-all-space --diff-algorithm=minimal --function-context --no-ext-diff --no-color {commit_id}^!&amp;#34;, hide=True, ).stdout, ) if diff.need_review: yield diff 调整提示词，携带 git diff 调用 GPT system_instruction = &amp;#34;&amp;#34;&amp;#34;你是一位资深的 IT 编程专家，精通 `Python 3.12`、`FastAPI`、`Pydantic`、`Shell` 和 `SQL`。你的任务是**严格审查**给定的 `diff` 内容，并找出其中存在的**重大问题**。 **重大问题** 定义如下： * **安全漏洞：** 例如 SQL 注入、跨站脚本 (XSS)、未授权访问等。 * **关键功能缺失：** 例如核心业务逻辑的缺失或错误实现。 * **语法/编译/运行时错误：** 导致代码无法运行或崩溃的错误。 * **性能瓶颈：** 导致程序运行缓慢或占用过多资源的瓶颈。 * **资源泄露：** 例如内存泄漏、文件句柄未关闭等。 * **关键逻辑错误：** 导致程序产生错误结果或行为的关键性错误。 **请忽略**以下类型的问题： * **一般问题：** 代码结构、可读性、潜在性能问题、代码风格、注释、未使用变量等不影响代码功能的因素。 * **存疑问题：** 需要更多上下文才能确认的问题。 * **`import` 语句相关的更改或缺失:** 例如新增、删除或修改 `import` 语句。 **请仅按以下格式回复：** 1. 重大问题： &amp;lt;重大问题列表，如果没有则回复“无”&amp;gt; 2. 建议： &amp;lt;针对重大问题的具体建议，如果没有则回复“无”&amp;gt; **重要：** 严格按照上述格式输出，只关注重大问题和建议，不要包含任何其他内容。 &amp;#34;&amp;#34;&amp;#34; quality_prompt = &amp;#34;&amp;#34;&amp;#34; __insert_diff__ &amp;#34;&amp;#34;&amp;#34; def init_openai_client() -&amp;gt; AsyncOpenAI: return AsyncOpenAI( api_key=get_config(&amp;#34;ai.openai.api_key&amp;#34;), base_url=get_config(&amp;#34;ai.openai.base_url&amp;#34;), default_headers={&amp;#34;Accept&amp;#34;: &amp;#34;text/event-stream&amp;#34;}, ) async def openai_chat_iter( messages: list[dict[str, str]], *, client: AsyncOpenAI = None, model: str = None, **options ) -&amp;gt; AsyncIterator[str]: client = client or init_openai_client() stream = client.chat.completions.create( messages=messages, model=model or get_config(&amp;#34;ai.openai.model&amp;#34;), **{ &amp;#34;temperature&amp;#34;: get_config(&amp;#34;ai.openai.temperature&amp;#34;), &amp;#34;timeout&amp;#34;: get_config(&amp;#34;ai.openai.timeout&amp;#34;) or 10, &amp;#34;stream&amp;#34;: True, **options, }, ) async for chunk in await stream: if chunk.choices and (content := chunk.choices[0].delta.content): yield content async def openai_chat(messages: list[dict[str, str]]) -&amp;gt; str: reply = [] async for chunk in openai_chat_iter(messages): reply.append(chunk) return &amp;#34;&amp;#34;.join(reply) @lru_cache(maxsize=1) def get_repository_name(): return run(r&amp;#34;git remote get-url origin | xargs basename | sed &amp;#39;s/\.git$//&amp;#39;&amp;#34;, hide=True).stdout.strip() async def quality_check(diff: CommitDiff): &amp;#34;&amp;#34;&amp;#34;质量检查&amp;amp;发送通知&amp;#34;&amp;#34;&amp;#34; messages = [ {&amp;#34;role&amp;#34;: &amp;#34;system&amp;#34;, &amp;#34;content&amp;#34;: system_instruction}, {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: quality_prompt.replace(&amp;#34;__insert_diff__&amp;#34;, diff.content)}, ] try: reply = await openai_chat(messages) except Exception as e: reply = f&amp;#34;Error: {e}&amp;#34; changes = f&amp;#34;xxx.git.com/{get_repository_name()}/-/compare/{diff.parent_rev}...{diff.rev}&amp;#34; content = f&amp;#34;&amp;#34;&amp;#34;@{diff.author} [Show changes]({changes}) {diff.changes} {reply}&amp;#34;&amp;#34;&amp;#34; # 发送 review 通知 send_notify(xxx) 你应该很容易能把上述片段代码集成到自己的 Workflow 里，成品效果如图，类似这种问题其实很容易在人肉 Review 的时候被忽略掉，但 AI 几乎不会漏。&#xA;</description><content:encoded><![CDATA[<p>最近在公司内网 CI 工作流里加入了 AI Codereview 的功能，效果确实不错，能发现一些常规 Review 很难发现的问题。废话不多说直接上步骤&代码。</p>
<ol>
<li>按 author 拆分 commit，获取 diff codes</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Python" data-lang="Python"><span class="line"><span class="cl"><span class="n">P_SKIP_CI</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">"\[(skip\s*ci|ci\s*skip|no\s*ci)]"</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">IGNORECASE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CommitCache</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="p">:</span> <span class="n">Path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">tempfile</span><span class="o">.</span><span class="n">gettempdir</span><span class="p">())</span> <span class="o">/</span> <span class="s2">"commits_cache.txt"</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">100</span>
</span></span><span class="line"><span class="cl">    <span class="n">ids</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">field</span><span class="p">(</span><span class="n">default_factory</span><span class="o">=</span><span class="nb">list</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">__post_init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">ids</span> <span class="o">=</span> <span class="p">[</span><span class="n">i</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span> <span class="k">if</span> <span class="n">i</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">ids</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">ids</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">ids</span> <span class="o">+</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">ids</span> <span class="k">if</span> <span class="n">i</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">ids</span><span class="p">])[</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">max_size</span> <span class="p">:]</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">ids</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CommitDiff</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">rev</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">author</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">content</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">parent_rev</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">field</span><span class="p">(</span><span class="n">init</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">__post_init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">parent_rev</span> <span class="o">=</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">"git rev-parse </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">rev</span><span class="si">}</span><span class="s2">^"</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@property</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">changes</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="sa">f</span><span class="s2">"""```
</span></span></span><span class="line"><span class="cl"><span class="si">{</span><span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">"git diff --name-only </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">rev</span><span class="si">}</span><span class="s2">^!"</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="si">}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">```"""</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@property</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">msg</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">"git log </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">rev</span><span class="si">}</span><span class="s2">^! --format=%B"</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@property</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">need_review</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">any</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">i</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"diff --git"</span><span class="p">)</span> <span class="ow">and</span> <span class="n">i</span><span class="o">.</span><span class="n">endswith</span><span class="p">((</span><span class="s2">".py"</span><span class="p">,</span> <span class="s2">".sh"</span><span class="p">))</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">P_SKIP_CI</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">prepare_commit_diff</span><span class="p">(</span><span class="n">log_range</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span> <span class="n">commit_range</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">3</span><span class="p">)</span> <span class="o">-></span> <span class="n">Iterable</span><span class="p">[</span><span class="n">CommitDiff</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">"""从指定(默认为最近10次)的提交中提取各 author 最近(默认为最近3次)的修改内容"""</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s1">'git log -n </span><span class="si">{</span><span class="n">log_range</span><span class="si">}</span><span class="s1"> --pretty=format:"%an" | sort | uniq'</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">commit_cache</span> <span class="o">=</span> <span class="n">CommitCache</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">author</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">author</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">"""git log --author="</span><span class="si">{</span><span class="n">author</span><span class="si">}</span><span class="s2">" --pretty=format:'%H' -n </span><span class="si">{</span><span class="n">commit_range</span><span class="si">}</span><span class="s2">"""</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">commit_id</span> <span class="ow">in</span> <span class="p">(</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span> <span class="k">if</span> <span class="n">i</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">commit_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">commit_cache</span><span class="o">.</span><span class="n">ids</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># 避免重复检查</span>
</span></span><span class="line"><span class="cl">                <span class="n">commit_cache</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">commit_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="n">diff</span> <span class="o">=</span> <span class="n">CommitDiff</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="n">author</span><span class="o">=</span><span class="n">author</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">rev</span><span class="o">=</span><span class="n">commit_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">content</span><span class="o">=</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                        <span class="sa">f</span><span class="s2">"git diff --ignore-all-space --diff-algorithm=minimal --function-context --no-ext-diff --no-color </span><span class="si">{</span><span class="n">commit_id</span><span class="si">}</span><span class="s2">^!"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">diff</span><span class="o">.</span><span class="n">need_review</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">yield</span> <span class="n">diff</span></span></span></code></pre></div></div>
<ol start="2">
<li>调整提示词，携带 git diff 调用 GPT</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Python" data-lang="Python"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">system_instruction</span> <span class="o">=</span> <span class="s2">"""你是一位资深的 IT 编程专家，精通 `Python 3.12`、`FastAPI`、`Pydantic`、`Shell` 和 `SQL`。你的任务是**严格审查**给定的 `diff` 内容，并找出其中存在的**重大问题**。
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">**重大问题** 定义如下：
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **安全漏洞：** 例如 SQL 注入、跨站脚本 (XSS)、未授权访问等。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **关键功能缺失：** 例如核心业务逻辑的缺失或错误实现。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **语法/编译/运行时错误：** 导致代码无法运行或崩溃的错误。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **性能瓶颈：**  导致程序运行缓慢或占用过多资源的瓶颈。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **资源泄露：** 例如内存泄漏、文件句柄未关闭等。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **关键逻辑错误：** 导致程序产生错误结果或行为的关键性错误。
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">**请忽略**以下类型的问题：
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **一般问题：** 代码结构、可读性、潜在性能问题、代码风格、注释、未使用变量等不影响代码功能的因素。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **存疑问题：**  需要更多上下文才能确认的问题。
</span></span></span><span class="line"><span class="cl"><span class="s2">*   **`import` 语句相关的更改或缺失:** 例如新增、删除或修改 `import` 语句。
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">**请仅按以下格式回复：**
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. 重大问题：
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    <重大问题列表，如果没有则回复“无”>
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">2. 建议：
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    <针对重大问题的具体建议，如果没有则回复“无”>
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">**重要：** 严格按照上述格式输出，只关注重大问题和建议，不要包含任何其他内容。
</span></span></span><span class="line"><span class="cl"><span class="s2">"""</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">quality_prompt</span> <span class="o">=</span> <span class="s2">"""
</span></span></span><span class="line"><span class="cl"><span class="s2">__insert_diff__
</span></span></span><span class="line"><span class="cl"><span class="s2">"""</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">init_openai_client</span><span class="p">()</span> <span class="o">-></span> <span class="n">AsyncOpenAI</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">AsyncOpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">api_key</span><span class="o">=</span><span class="n">get_config</span><span class="p">(</span><span class="s2">"ai.openai.api_key"</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">base_url</span><span class="o">=</span><span class="n">get_config</span><span class="p">(</span><span class="s2">"ai.openai.base_url"</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">default_headers</span><span class="o">=</span><span class="p">{</span><span class="s2">"Accept"</span><span class="p">:</span> <span class="s2">"text/event-stream"</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">openai_chat_iter</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]],</span> <span class="o">*</span><span class="p">,</span> <span class="n">client</span><span class="p">:</span> <span class="n">AsyncOpenAI</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span> <span class="n">model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-></span> <span class="n">AsyncIterator</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span> <span class="o">=</span> <span class="n">client</span> <span class="ow">or</span> <span class="n">init_openai_client</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">completions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">messages</span><span class="o">=</span><span class="n">messages</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">model</span><span class="o">=</span><span class="n">model</span> <span class="ow">or</span> <span class="n">get_config</span><span class="p">(</span><span class="s2">"ai.openai.model"</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="o">**</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"temperature"</span><span class="p">:</span> <span class="n">get_config</span><span class="p">(</span><span class="s2">"ai.openai.temperature"</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"timeout"</span><span class="p">:</span> <span class="n">get_config</span><span class="p">(</span><span class="s2">"ai.openai.timeout"</span><span class="p">)</span> <span class="ow">or</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"stream"</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="o">**</span><span class="n">options</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="k">await</span> <span class="n">stream</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">chunk</span><span class="o">.</span><span class="n">choices</span> <span class="ow">and</span> <span class="p">(</span><span class="n">content</span> <span class="o">:=</span> <span class="n">chunk</span><span class="o">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">delta</span><span class="o">.</span><span class="n">content</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">yield</span> <span class="n">content</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">openai_chat</span><span class="p">(</span><span class="n">messages</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]])</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">reply</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">openai_chat_iter</span><span class="p">(</span><span class="n">messages</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">reply</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">""</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">reply</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@lru_cache</span><span class="p">(</span><span class="n">maxsize</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_repository_name</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">run</span><span class="p">(</span><span class="sa">r</span><span class="s2">"git remote get-url origin | xargs basename | sed 's/\.git$//'"</span><span class="p">,</span> <span class="n">hide</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">quality_check</span><span class="p">(</span><span class="n">diff</span><span class="p">:</span> <span class="n">CommitDiff</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">"""质量检查&发送通知"""</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="s2">"role"</span><span class="p">:</span> <span class="s2">"system"</span><span class="p">,</span> <span class="s2">"content"</span><span class="p">:</span> <span class="n">system_instruction</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="s2">"role"</span><span class="p">:</span> <span class="s2">"user"</span><span class="p">,</span> <span class="s2">"content"</span><span class="p">:</span> <span class="n">quality_prompt</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"__insert_diff__"</span><span class="p">,</span> <span class="n">diff</span><span class="o">.</span><span class="n">content</span><span class="p">)},</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">reply</span> <span class="o">=</span> <span class="k">await</span> <span class="n">openai_chat</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">reply</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"Error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">changes</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"xxx.git.com/</span><span class="si">{</span><span class="n">get_repository_name</span><span class="p">()</span><span class="si">}</span><span class="s2">/-/compare/</span><span class="si">{</span><span class="n">diff</span><span class="o">.</span><span class="n">parent_rev</span><span class="si">}</span><span class="s2">...</span><span class="si">{</span><span class="n">diff</span><span class="o">.</span><span class="n">rev</span><span class="si">}</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl">    <span class="n">content</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"""@</span><span class="si">{</span><span class="n">diff</span><span class="o">.</span><span class="n">author</span><span class="si">}</span><span class="s2"> [Show changes](</span><span class="si">{</span><span class="n">changes</span><span class="si">}</span><span class="s2">)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="si">{</span><span class="n">diff</span><span class="o">.</span><span class="n">changes</span><span class="si">}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="si">{</span><span class="n">reply</span><span class="si">}</span><span class="s2">"""</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 发送 review 通知</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_notify</span><span class="p">(</span><span class="n">xxx</span><span class="p">)</span></span></span></code></pre></div></div>
<p>你应该很容易能把上述片段代码集成到自己的 Workflow 里，成品效果如图，类似这种问题其实很容易在人肉 Review 的时候被忽略掉，但 AI 几乎不会漏。</p>
<figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt="image"
    src="https://github.com/user-attachments/assets/a38a8d86-abcc-4f6a-bbd8-89eb3ee5a573"
    ></figure>
<p>这玩意其实缺点也很明显，受限于上下文，基本上偏业务逻辑都是瞎点评，但总体作用是积极的，能实际提高 codereview 的效率。</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">06</span><span class="nx">T14</span><span class="o">:</span><span class="mi">55</span><span class="o">:</span><span class="mi">44</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">26</span><span class="nx">T03</span><span class="o">:</span><span class="mi">59</span><span class="o">:</span><span class="mi">13</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/81
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>水一发GoDaddy转域名到Cloudflare的坑</title>
      <link>https://blog.ferstar.org/posts/transfer-domain-godaddy-cloudflare/</link>
      <pubDate>Wed, 10 Jan 2024 03:10:50 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/transfer-domain-godaddy-cloudflare/</guid>
      <description>域名迁移其实蛮常规的，只有一个坑点一定要注意：&#xA;Domain Transfer Out 的这个窗口从迁移开始到结束必须开着，别关，关了就是莫名其妙的错误，贼坑。&#xA;# NOTE: I am not responsible for any expired content. create@2024-01-10T03:10:50+08:00 update@2024-01-10T03:13:34+08:00 comment@https://github.com/ferstar/blog/issues/80</description><content:encoded><![CDATA[<p>域名迁移其实蛮常规的，只有一个坑点一定要注意：</p>
<p>Domain Transfer Out 的这个窗口从迁移开始到结束必须开着，别关，关了就是莫名其妙的错误，贼坑。</p>
<figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt="image"
    src="https://github.com/ferstar/blog/assets/2854276/48aac3c2-3493-4a91-be8c-6bc355ec4f76"
    ></figure>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2024-01-10T03:10:50+08:00
</span></span><span class="line"><span class="cl">update@2024-01-10T03:13:34+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/80</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Try github self-hosted action runner</title>
      <link>https://blog.ferstar.org/posts/github-self-hosted-runner-setup/</link>
      <pubDate>Sat, 02 Sep 2023 04:23:11 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/github-self-hosted-runner-setup/</guid>
      <description>详解 GitHub Actions Self-hosted Runner 生产环境部署方案，解决由于网络隔离或资源受限导致的 CI/CD 瓶颈，实现高效本地自动化构建。</description><content:encoded><![CDATA[<p>Official docs: <a href="https://docs.github.com/en/actions/hosting-your-own-runners"  target="_blank" rel="noreferrer">https://docs.github.com/en/actions/hosting-your-own-runners</a></p>
<p>Simple docker compose file:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">'3.8'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">worker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">myoung34/github-runner:ubuntu-bionic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REPO_URL</span><span class="p">:</span><span class="w"> </span><span class="l">${RUNNER_REPO}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">RUNNER_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">${RUNNER_NAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">RUNNER_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${RUNNER_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CONFIGURED_ACTIONS_RUNNER_FILES_DIR</span><span class="p">:</span><span class="w"> </span><span class="l">${CONFIGURED_ACTIONS_RUNNER_FILES_DIR}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DISABLE_AUTOMATIC_DEREGISTRATION</span><span class="p">:</span><span class="w"> </span><span class="l">${DISABLE_AUTOMATIC_DEREGISTRATION}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">RUNNER_WORKDIR</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp/runner/work</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ORG_RUNNER</span><span class="p">:</span><span class="w"> </span><span class="s1">'false'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LABELS</span><span class="p">:</span><span class="w"> </span><span class="l">linux,x64,home-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># needed on SELinux systems to allow docker container to manage other docker containers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'/var/run/docker.sock:/var/run/docker.sock'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'/tmp/runner:/tmp/runner'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'./data:/data'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># note: a quirk of docker-in-docker is that this path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># needs to be the same path on host and inside the container,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># docker mgmt cmds run outside of docker but expect the paths from within</span></span></span></code></pre></div></div>
<p>Your <code>.env</code> file may be like this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">RUNNER_REPO</span><span class="o">=</span>https://github.com/user/repo
</span></span><span class="line"><span class="cl"><span class="nv">RUNNER_TOKEN</span><span class="o">=</span>NOT THE GITHUB ACCESS TOKEN
</span></span><span class="line"><span class="cl"><span class="nv">RUNNER_NAME</span><span class="o">=</span>foo
</span></span><span class="line"><span class="cl"><span class="nv">CONFIGURED_ACTIONS_RUNNER_FILES_DIR</span><span class="o">=</span>/data
</span></span><span class="line"><span class="cl"><span class="nv">DISABLE_AUTOMATIC_DEREGISTRATION</span><span class="o">=</span>true</span></span></code></pre></div></div>
<p>Please note that the <code>RUNNER_TOKEN</code> is not your github access token</p>
<ol>
<li>visit <a href="https://github.com/user/repo/settings/actions/runners/new"  target="_blank" rel="noreferrer">https://github.com/user/repo/settings/actions/runners/new</a></li>
<li>get your action’s real token</li>
</ol>
<figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt="image"
    src="https://github.com/ferstar/blog/assets/2854276/7d6a6fbe-0812-4299-91b3-a2dc2d595167"
    ></figure>
<p>Some normal running logs:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker-compose up
</span></span><span class="line"><span class="cl">Recreating github-actions_worker_1 ... <span class="k">done</span>
</span></span><span class="line"><span class="cl">Attaching to github-actions_worker_1
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> Runner reusage is disabled
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> Configuring
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> --------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>        ____ _ _   _   _       _          _        _   _                      <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>       / ___<span class="o">(</span>_<span class="o">)</span> <span class="p">|</span>_<span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="p">|</span>_   _<span class="p">|</span> <span class="p">|</span>__      / <span class="se">\ </span>  ___<span class="p">|</span> <span class="p">|</span>_<span class="o">(</span>_<span class="o">)</span> ___  _ __  ___      <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>      <span class="p">|</span> <span class="p">|</span>  _<span class="p">|</span> <span class="p">|</span> __<span class="p">|</span> <span class="p">|</span>_<span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="s1">'_ \    / _ \ / __| __| |/ _ \| '</span>_ <span class="se">\/</span> __<span class="p">|</span>     <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>      <span class="p">|</span> <span class="p">|</span>_<span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="p">|</span>_<span class="p">|</span>  _  <span class="p">|</span> <span class="p">|</span>_<span class="p">|</span> <span class="p">|</span> <span class="p">|</span>_<span class="o">)</span> <span class="p">|</span>  / ___ <span class="se">\ </span><span class="o">(</span>__<span class="p">|</span> <span class="p">|</span>_<span class="p">|</span> <span class="p">|</span> <span class="o">(</span>_<span class="o">)</span> <span class="p">|</span> <span class="p">|</span> <span class="p">|</span> <span class="se">\_</span>_ <span class="se">\ </span>    <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>       <span class="se">\_</span>___<span class="p">|</span>_<span class="p">|</span><span class="se">\_</span>_<span class="p">|</span>_<span class="p">|</span> <span class="p">|</span>_<span class="p">|</span><span class="se">\_</span>_,_<span class="p">|</span>_.__/  /_/   <span class="se">\_\_</span>__<span class="p">|</span><span class="se">\_</span>_<span class="p">|</span>_<span class="p">|</span><span class="se">\_</span>__/<span class="p">|</span>_<span class="p">|</span> <span class="p">|</span>_<span class="p">|</span>___/     <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>                                                                              <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>                       Self-hosted runner registration                        <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="p">|</span>                                                                              <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> --------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="c1"># Authentication</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> √ Connected to GitHub
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="c1"># Runner Registration</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> √ Runner successfully added
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> √ Runner connection is good
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> <span class="c1"># Runner settings</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> √ Settings Saved.
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> √ Connected to GitHub
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> Current runner version: <span class="s1">'2.308.0'</span>
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> 2023-09-02 03:22:29Z: Listening <span class="k">for</span> Jobs
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> 2023-09-02 03:23:08Z: Running job: build_deb
</span></span><span class="line"><span class="cl">worker_1  <span class="p">|</span> 2023-09-02 03:26:51Z: Job build_deb completed with result: Succeeded</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-09-02T04:23:11+08:00
</span></span><span class="line"><span class="cl">update@2023-09-02T05:21:40+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/79</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>信创之从PostgreSQL到GaussDB</title>
      <link>https://blog.ferstar.org/posts/migrate-postgresql-gaussdb/</link>
      <pubDate>Thu, 15 Jun 2023 06:19:44 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/migrate-postgresql-gaussdb/</guid>
      <description>信创是个啥玩意具体我懒得说了，简单记录下作为中年 CRUD 仔给 Web 后端换 DB 这件事。&#xA;情况大概是这样：&#xA;我们 ORM 是 Gino&#xA;然后 Gino 依赖 asyncpg 来链接 PostgreSQL，换 GaussDB 的话，由于这玩意是华为从 PostgreSQL 一个古早版本 9.2.4 一通魔改糊起来的，一些特性并不支持，所以这一步就掉坑了：&#xA;</description><content:encoded><![CDATA[<blockquote><p>信创是个啥玩意具体我懒得说了，简单记录下作为中年 CRUD 仔给 Web 后端换 DB 这件事。</p>
</blockquote><p>情况大概是这样：</p>
<p>我们 ORM 是 <a href="https://snyk.io/advisor/python/gino"  target="_blank" rel="noreferrer">Gino</a></p>
<p>然后 Gino 依赖 <a href="https://github.com/MagicStack/asyncpg"  target="_blank" rel="noreferrer">asyncpg</a> 来链接 PostgreSQL，换 GaussDB 的话，由于这玩意是华为从 PostgreSQL 一个古早版本 9.2.4 一通魔改糊起来的，一些特性并不支持，所以这一步就掉坑了：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">    <span class="k">return</span> await self._protocol.query<span class="o">(</span>query, timeout<span class="o">)</span>
</span></span><span class="line"><span class="cl">  File <span class="s2">"asyncpg/protocol/protocol.pyx"</span>, line 338, in query
</span></span><span class="line"><span class="cl">asyncpg.exceptions.FeatureNotSupportedError: UNLISTEN statement is not yet supported.</span></span></code></pre></div></div>
<p>查了下华为云的说明：</p>
<p><a href="https://support.huaweicloud.com/intl/en-us/sqlreference-dws/dws_06_0006.html"  target="_blank" rel="noreferrer">https://support.huaweicloud.com/intl/en-us/sqlreference-dws/dws_06_0006.html</a></p>
<p>嗯，发现不支持 notification 相关特性，那么看来要小改下 asyncpg，见：</p>
<p><a href="https://github.com/ferstar/asyncpg/commit/0be6023443346213a26795a27ef3add7abc79aea"  target="_blank" rel="noreferrer">https://github.com/ferstar/asyncpg/commit/0be6023443346213a26795a27ef3add7abc79aea</a></p>
<p>让 asyncpg 识别到 GaussDB 以后关几个 feature 即可，这里的 server_settings 可以通过初始化 DB Pool 的时候传过去。</p>
<p>我们通过 <a href="https://alembic.sqlalchemy.org/"  target="_blank" rel="noreferrer">alembic</a> 来做 DB migration，这个环节也出现了类似的问题，就是人家不认这个 GaussDB，解决也简单：改到让他认。</p>
<p>见：https://github.com/sqlalchemy/sqlalchemy/commit/3a0794ce455201a219a5e8491a9c346d69558ad9</p>
<p>然后正常连接是没有问题了，但跑到一些 GaussDB 不支持的迁移脚本时就又扑街：项目用到了 gin 的 extension，看一时半会估计是不太会兼容了，所以么办法只能改写用普通索引。</p>
<p>接下来，项目初始化、DB migration都顺利完成，于是就跑了一发单元测试，再次被打脸：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ProgrammingError: syntax error at or near <span class="s2">"ON  CONFLICT ("</span></span></span></code></pre></div></div>
<p>也就是说<code>UPDATE ... ON CONFLICT ... DO ...</code>这种也是不支持，要改也简单：先查冲突，然后再更新。</p>
<p>一通操作后，项目总算正常运行。后续有几个需要注意的问题：</p>
<ol>
<li>代码要考虑兼容性：从base version来看，横跨了好几个大版本，基本上告别 PostgreSQL 新特性</li>
<li>DB 相关组件也要考虑兼容性，自己订制魔改的部分，几乎不太可能被主线合并</li>
<li>组员代码审核：需要维护一个lint机制来自动检测组员提交的 DB 相关代码的兼容性</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-06-15T06:19:44+08:00
</span></span><span class="line"><span class="cl">update@2023-06-15T06:19:44+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/78</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Migrate from Tornado to FastAPI</title>
      <link>https://blog.ferstar.org/posts/migrating-legacy-tornado-to-fastapi/</link>
      <pubDate>Fri, 09 Jun 2023 12:32:25 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/migrating-legacy-tornado-to-fastapi/</guid>
      <description>心水 FastAPI 的 ASGI、依赖注入、类型注解和自动在线文档很久，但迫于项目历史依赖（立项的时候 FastAPI 还没出来），迟迟没有迁移的动力。直到某次又翻到过期的接口文档，实在不想继续过文档和代码分割的日子了，于是决定开整。&#xA;</description><content:encoded><![CDATA[<p>心水 FastAPI 的 ASGI、依赖注入、类型注解和自动在线文档很久，但迫于项目历史依赖（立项的时候 FastAPI 还没出来），迟迟没有迁移的动力。直到某次又翻到过期的接口文档，实在不想继续过文档和代码分割的日子了，于是决定开整。</p>
<p>迫于旧接口太多，一下迁移明显不现实，想了两个过渡方案：</p>
<ol>
<li>Nginx 分流，旧接口继续使用 Tornado 驱动，新接口走 FastAPI</li>
<li>完全抛弃 Tornado 驱动，利用 WSGI 将旧接口分流、转换成 Tornado 兼容的响应</li>
</ol>
<p>方案一实施起来最简单，但有个问题是 Session 共享不好解决，另外增加了额外的运维部署复杂度，于是pass；</p>
<p>走方案二的话，就需要写一个中间件来将旧接口分流、转换给原 Tornado 的实现，代码如下：</p>
<p>经典的 Tornado hello world</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">tornado</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MainHandler</span><span class="p">(</span><span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">RequestHandler</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"Hello, world"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">make_app</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">Application</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="sa">r</span><span class="s2">"/api/v1/"</span><span class="p">,</span> <span class="n">MainHandler</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span> <span class="o">=</span> <span class="n">make_app</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">listen</span><span class="p">(</span><span class="mi">8888</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Event</span><span class="p">()</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span></span></span></code></pre></div></div>
<p>本来是打算手撸 ASGI -> WSGI 的，刚好刷推看到  Django 的 a2wsgi，能省不少手撕 WSGI 协议的活，于是果断抄之，最终成品如下：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">contextvars</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">functools</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">urllib.parse</span> <span class="k">as</span> <span class="nn">urllib_parse</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">asyncio</span> <span class="kn">import</span> <span class="n">Future</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Tuple</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">tornado</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">uvicorn</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">a2wsgi.types</span> <span class="kn">import</span> <span class="n">Environ</span><span class="p">,</span> <span class="n">StartResponse</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">a2wsgi.wsgi</span> <span class="kn">import</span> <span class="n">Body</span><span class="p">,</span> <span class="n">WSGIResponder</span><span class="p">,</span> <span class="n">build_environ</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">starlette.types</span> <span class="kn">import</span> <span class="n">Receive</span><span class="p">,</span> <span class="n">Scope</span><span class="p">,</span> <span class="n">Send</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">tornado</span> <span class="kn">import</span> <span class="n">httputil</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">tornado.escape</span> <span class="kn">import</span> <span class="n">native_str</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">tornado.web</span> <span class="kn">import</span> <span class="n">Application</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># WSGI has no facilities for flow control, so just return an already-done</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Future when the interface requires it.</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_dummy_future</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span> <span class="o">=</span> <span class="n">Future</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">set_result</span><span class="p">(</span><span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">f</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">_WSGIRequestContext</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">remote_ip</span><span class="p">,</span> <span class="n">protocol</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">remote_ip</span> <span class="o">=</span> <span class="n">remote_ip</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">protocol</span> <span class="o">=</span> <span class="n">protocol</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">remote_ip</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">_WSGIConnection</span><span class="p">(</span><span class="n">httputil</span><span class="o">.</span><span class="n">HTTPConnection</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">start_response</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">method</span> <span class="o">=</span> <span class="n">method</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">start_response</span> <span class="o">=</span> <span class="n">start_response</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="o">=</span> <span class="n">context</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_write_buffer</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_finished</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_error</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">set_close_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">callback</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># WSGI has no facility for detecting a closed connection mid-request,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># so we can simply ignore the callback.</span>
</span></span><span class="line"><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">write_headers</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">start_line</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">chunk</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">callback</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s2">"HEAD"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="s2">"Content-Length"</span> <span class="ow">in</span> <span class="n">headers</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Length"</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">start_response</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"</span><span class="si">%d</span><span class="s2"> </span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">start_line</span><span class="o">.</span><span class="n">code</span><span class="p">,</span> <span class="n">start_line</span><span class="o">.</span><span class="n">reason</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="p">[(</span><span class="n">native_str</span><span class="p">(</span><span class="n">k</span><span class="p">),</span> <span class="n">native_str</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">headers</span><span class="o">.</span><span class="n">get_all</span><span class="p">()],</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">chunk</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">callback</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">callback</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">callback</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">_dummy_future</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">write</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">chunk</span><span class="p">,</span> <span class="n">callback</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">-=</span> <span class="nb">len</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o"><</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">_error</span> <span class="o">=</span> <span class="n">httputil</span><span class="o">.</span><span class="n">HTTPOutputError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">"Tried to write more data than Content-Length"</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">raise</span> <span class="bp">self</span><span class="o">.</span><span class="n">_error</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_write_buffer</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">callback</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">callback</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">_dummy_future</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">finish</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">            <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span> <span class="o">!=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_error</span> <span class="o">=</span> <span class="n">httputil</span><span class="o">.</span><span class="n">HTTPOutputError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="sa">f</span><span class="s2">"Tried to write </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_expected_content_remaining</span><span class="si">}</span><span class="s2"> bytes less than Content-Length"</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="bp">self</span><span class="o">.</span><span class="n">_error</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_finished</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">WSGIAdapter</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">app</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">:</span> <span class="n">Environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">:</span> <span class="n">StartResponse</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">-></span> <span class="n">List</span><span class="p">[</span><span class="nb">bytes</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="n">method</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"REQUEST_METHOD"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">uri</span> <span class="o">=</span> <span class="n">urllib_parse</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"SCRIPT_NAME"</span><span class="p">,</span> <span class="s2">""</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">uri</span> <span class="o">+=</span> <span class="n">urllib_parse</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"PATH_INFO"</span><span class="p">,</span> <span class="s2">""</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"QUERY_STRING"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">uri</span> <span class="o">+=</span> <span class="s2">"?"</span> <span class="o">+</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"QUERY_STRING"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">headers</span> <span class="o">=</span> <span class="n">httputil</span><span class="o">.</span><span class="n">HTTPHeaders</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"CONTENT_TYPE"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"CONTENT_TYPE"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"CONTENT_LENGTH"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Length"</span><span class="p">]</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"CONTENT_LENGTH"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">environ</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">key</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"HTTP_"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="n">headers</span><span class="p">[</span><span class="n">key</span><span class="p">[</span><span class="mi">5</span><span class="p">:]</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"_"</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">)]</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"Content-Length"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">body</span> <span class="o">=</span> <span class="k">await</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"wsgi.input"</span><span class="p">]</span><span class="o">.</span><span class="n">aread</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">headers</span><span class="p">[</span><span class="s2">"Content-Length"</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">body</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
</span></span><span class="line"><span class="cl">        <span class="n">protocol</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"wsgi.url_scheme"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">remote_ip</span> <span class="o">=</span> <span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"REMOTE_ADDR"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"HTTP_HOST"</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">host</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"HTTP_HOST"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">host</span> <span class="o">=</span> <span class="n">environ</span><span class="p">[</span><span class="s2">"SERVER_NAME"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">connection</span> <span class="o">=</span> <span class="n">_WSGIConnection</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">method</span><span class="p">,</span> <span class="n">start_response</span><span class="p">,</span> <span class="n">_WSGIRequestContext</span><span class="p">(</span><span class="n">remote_ip</span><span class="p">,</span> <span class="n">protocol</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">request</span> <span class="o">=</span> <span class="n">httputil</span><span class="o">.</span><span class="n">HTTPServerRequest</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">method</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">uri</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"HTTP/1.1"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">body</span><span class="o">=</span><span class="n">body</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">host</span><span class="o">=</span><span class="n">host</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">connection</span><span class="o">=</span><span class="n">connection</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">request</span><span class="o">.</span><span class="n">_parse_body</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">connection</span><span class="o">.</span><span class="n">_error</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">connection</span><span class="o">.</span><span class="n">_error</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">connection</span><span class="o">.</span><span class="n">_finished</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">Exception</span><span class="p">(</span><span class="s2">"request did not finish synchronously"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">connection</span><span class="o">.</span><span class="n">_write_buffer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TornadoMiddleware</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">WSGIAdapter</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">executor</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">:</span> <span class="n">Scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">:</span> <span class="n">Receive</span><span class="p">,</span> <span class="n">send</span><span class="p">:</span> <span class="n">Send</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">scope</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"http"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">responder</span> <span class="o">=</span> <span class="n">_WSGIResponder</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">executor</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="k">await</span> <span class="n">responder</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">scope</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"websocket"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">send</span><span class="p">({</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"websocket.close"</span><span class="p">,</span> <span class="s2">"code"</span><span class="p">:</span> <span class="mi">1000</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">_Body</span><span class="p">(</span><span class="n">Body</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">_areceive_more_data</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_has_more</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="sa">b</span><span class="s2">""</span>
</span></span><span class="line"><span class="cl">        <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">receive</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_has_more</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"more_body"</span><span class="p">,</span> <span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">message</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"body"</span><span class="p">,</span> <span class="sa">b</span><span class="s2">""</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">aread</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="n">size</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="ow">or</span> <span class="n">size</span> <span class="o">></span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">_areceive_more_data</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_has_more</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">break</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">size</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">result</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">result</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="p">[:</span><span class="n">size</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">buffer</span><span class="p">[:</span><span class="n">size</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">_WSGIResponder</span><span class="p">(</span><span class="n">WSGIResponder</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">:</span> <span class="n">Scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">:</span> <span class="n">Receive</span><span class="p">,</span> <span class="n">send</span><span class="p">:</span> <span class="n">Send</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="n">_Body</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">loop</span><span class="p">,</span> <span class="n">receive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">environ</span> <span class="o">=</span> <span class="n">build_environ</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">sender</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">sender</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">loop</span><span class="o">.</span><span class="n">create_task</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">sender</span><span class="p">(</span><span class="n">send</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">context</span> <span class="o">=</span> <span class="n">contextvars</span><span class="o">.</span><span class="n">copy_context</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">func</span> <span class="o">=</span> <span class="n">functools</span><span class="o">.</span><span class="n">partial</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">run</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">awsgi</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">func</span><span class="p">(</span><span class="n">environ</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">start_response</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">send_queue</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">send_event</span><span class="o">.</span><span class="n">set</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">wait_for</span><span class="p">(</span><span class="n">sender</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">exc_info</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">raise</span> <span class="bp">self</span><span class="o">.</span><span class="n">exc_info</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">with_traceback</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="bp">self</span><span class="o">.</span><span class="n">exc_info</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">exc_info</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">sender</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">sender</span><span class="o">.</span><span class="n">done</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                <span class="n">sender</span><span class="o">.</span><span class="n">cancel</span><span class="p">()</span>  <span class="c1"># pragma: no cover</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">start_response</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">status</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">response_headers</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">Tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]],</span>
</span></span><span class="line"><span class="cl">        <span class="n">exc_info</span><span class="p">:</span> <span class="n">Any</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">exc_info</span> <span class="o">=</span> <span class="n">exc_info</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">response_started</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">response_started</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">            <span class="n">status_code_string</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">status</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">status_code</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">status_code_string</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">headers</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="p">(</span><span class="n">name</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"latin1"</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="n">value</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"latin1"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">response_headers</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">send</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"http.response.start"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">"status"</span><span class="p">:</span> <span class="n">status_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">"headers"</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">awsgi</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">environ</span><span class="p">:</span> <span class="n">Environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">:</span> <span class="n">StartResponse</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">(</span><span class="n">environ</span><span class="p">,</span> <span class="n">start_response</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">send</span><span class="p">({</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"http.response.body"</span><span class="p">,</span> <span class="s2">"body"</span><span class="p">:</span> <span class="n">chunk</span><span class="p">,</span> <span class="s2">"more_body"</span><span class="p">:</span> <span class="kc">True</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">send</span><span class="p">({</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"http.response.body"</span><span class="p">,</span> <span class="s2">"body"</span><span class="p">:</span> <span class="sa">b</span><span class="s2">""</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">HandleDelegate</span><span class="p">(</span><span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">_HandlerDelegate</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"compiled_template_cache"</span><span class="p">,</span> <span class="kc">True</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">with</span> <span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">RequestHandler</span><span class="o">.</span><span class="n">_template_loader_lock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">for</span> <span class="n">loader</span> <span class="ow">in</span> <span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">RequestHandler</span><span class="o">.</span><span class="n">_template_loaders</span><span class="o">.</span><span class="n">values</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                    <span class="n">loader</span><span class="o">.</span><span class="n">reset</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"static_hash_cache"</span><span class="p">,</span> <span class="kc">True</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">StaticFileHandler</span><span class="o">.</span><span class="n">reset</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">handler</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">handler_class</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">application</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="o">**</span><span class="bp">self</span><span class="o">.</span><span class="n">handler_kwargs</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">transforms</span> <span class="o">=</span> <span class="p">[</span><span class="n">t</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">)</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">transforms</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">handler</span><span class="o">.</span><span class="n">_execute</span><span class="p">(</span><span class="n">transforms</span><span class="p">,</span> <span class="o">*</span><span class="bp">self</span><span class="o">.</span><span class="n">path_args</span><span class="p">,</span> <span class="o">**</span><span class="bp">self</span><span class="o">.</span><span class="n">path_kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TornadoApplication</span><span class="p">(</span><span class="n">Application</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get_handler_delegate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">request</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">target_class</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">target_kwargs</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">path_args</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">path_kwargs</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">HandleDelegate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">target_class</span><span class="p">,</span> <span class="n">target_kwargs</span><span class="p">,</span> <span class="n">path_args</span><span class="p">,</span> <span class="n">path_kwargs</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MainHandler</span><span class="p">(</span><span class="n">tornado</span><span class="o">.</span><span class="n">web</span><span class="o">.</span><span class="n">RequestHandler</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"Hello, world"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">make_app</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">TornadoApplication</span><span class="p">([(</span><span class="sa">r</span><span class="s2">"/api/v1/"</span><span class="p">,</span> <span class="n">MainHandler</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span> <span class="o">=</span> <span class="n">make_app</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">listen</span><span class="p">(</span><span class="mi">8888</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Event</span><span class="p">()</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="o">.</span><span class="n">mount</span><span class="p">(</span><span class="s2">"/api/v1"</span><span class="p">,</span> <span class="n">TornadoMiddleware</span><span class="p">(</span><span class="n">make_app</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.get</span><span class="p">(</span><span class="s2">"/api/v2/"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hello_world</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">"message"</span><span class="p">:</span> <span class="s2">"Hello, World!"</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">uvicorn</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8888</span><span class="p">)</span></span></span></code></pre></div></div>
<p>测一下：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">~ curl http://127.0.0.1:8888/api/v2/
</span></span><span class="line"><span class="cl"><span class="o">{</span><span class="s2">"message"</span>:<span class="s2">"Hello, World!"</span><span class="o">}</span>%
</span></span><span class="line"><span class="cl">~ curl http://127.0.0.1:8888/api/v1/
</span></span><span class="line"><span class="cl">Hello, world%</span></span></code></pre></div></div>
<p>可以看出 v1 接口请求被 FastAPI 通过中间件机制传给 TornadoMiddleware，再由 TornadoMiddleware 完成 ASGI 到 WSGI 再到 Tornado 接口代码的转换工作，完美解决了旧 Tornado 接口与新 FastAPI 接口共存的问题，且不影响现有项目部署流程，十分nice。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-06-09T12:32:25+08:00
</span></span><span class="line"><span class="cl">update@2023-12-27T11:08:32+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/77</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>如何在Linux X11开启R9000P2021款笔记本165Hz刷新率</title>
      <link>https://blog.ferstar.org/posts/r9000p-165hz-linux-x11/</link>
      <pubDate>Tue, 25 Apr 2023 02:40:04 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/r9000p-165hz-linux-x11/</guid>
      <description>解决联想拯救者 R9000P 在 Linux X11 环境下刷新率锁定或无法开启 165Hz 的问题，优化高刷显示效果，提升系统交互流畅度。</description><content:encoded><![CDATA[<p>我这款本子是 3050Ti 的显卡，Linux 下偶尔炼个超迷你小丹凑合用一下，平常大部分时候都是闲置状态，所以多数情况用的是<strong>混合模式</strong>而非<strong>独显直通</strong>。</p>
<p>这样有个问题，不管是 Archlinux 还是 Ubuntu，进桌面显示器配置就只有 60Hz 的选项，但切到<strong>独显直通</strong>，165Hz 的高刷就又有了，一通搜索以后，得出结论：在混合模式下系统无法准确获取EDID文件。另外即使是<strong>独显直通</strong>状态，屏幕回报的刷新率是 165.02Hz，说明这块屏幕不是标准的 165Hz 行刷新率，操作系统并没有正确的屏幕分辨率信息。</p>
<p>我在这里 <a href="https://wiki.archlinux.org/title/xrandr#Troubleshooting"  target="_blank" rel="noreferrer">https://wiki.archlinux.org/title/xrandr#Troubleshooting</a> 找到了这个问题的解决办法：</p>
<p>先在<strong>独显直通</strong>状态，通过<code>xrandr --verbose</code>记下 60、165Hz 的 Modeline，我的机器是这样的：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># 60Hz</span>
</span></span><span class="line"><span class="cl">282.7 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync
</span></span><span class="line"><span class="cl"><span class="c1"># 165Hz</span>
</span></span><span class="line"><span class="cl">777.410 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync</span></span></code></pre></div></div>
<p>接下来就是把这两种配置用<code>xrandr</code>添加到内屏配置上，由于<strong>混合模式</strong>和<strong>独显直通</strong>两种模式下，内屏设备名并不固定，可以通过如下命令找到：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">xrandr <span class="p">|</span> grep -i <span class="s1">' connected'</span> <span class="p">|</span> cut -d <span class="s1">' '</span> -f <span class="m">1</span></span></span></code></pre></div></div>
<p>我的机器输出如下：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">eDP-1
</span></span><span class="line"><span class="cl">DP-1-0  <span class="c1"># 这个其实是我的外接屏幕</span></span></span></code></pre></div></div>
<p>确定好内屏设备名（eDP-1）以后，就可以来指定我们上面探测到的分辨率配置了：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># 65Hz</span>
</span></span><span class="line"><span class="cl">xrandr --newmode <span class="s2">"2560x1600_60.00"</span> 282.7 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync
</span></span><span class="line"><span class="cl">xrandr --addmode eDP-1 2560x1600_60.00
</span></span><span class="line"><span class="cl"><span class="c1"># 165Hz</span>
</span></span><span class="line"><span class="cl">xrandr --newmode <span class="s2">"2560x1600_165.00"</span> 777.410 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync
</span></span><span class="line"><span class="cl">xrandr --addmode eDP-1 2560x1600_165.00</span></span></code></pre></div></div>
<p>此时打开显示器配置，熟悉的高刷分辨率就回来了，且可以自由切换。</p>
<figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt="image"
    src="https://user-images.githubusercontent.com/2854276/234154957-dab73e59-5526-40bd-a1be-8e5ebdf0f103.png"
    ></figure>
<p>当然你也可以继续用<code>xrandr</code>在命令行切换当前显示器的刷新率：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">xrandr --output eDP-1 --mode <span class="s2">"2560x1600_165.00"</span>
</span></span><span class="line"><span class="cl">xrandr --output eDP-1 --mode <span class="s2">"2560x1600_60.00"</span></span></span></code></pre></div></div>
<p>以上临时注入的配置在重启以后就会消失，Archlinux Wiki 里贴心的写了持久化方案，也就是把配置写到 xorg 文件里。作为懒癌患者，因为偶尔还有不插电的使用场景，所以当然是要搞成自动挡啦：插电 165、电池 60。那么就有两个问题需要解决：</p>
<ol>
<li>插电、离电条件触发</li>
<li>写个切换分辨率的脚本</li>
</ol>
<p>KDE 的系统配置==》电源管理==》节能==》（交流供电、电池供电）运行脚本刚好可以解决脚本触发条件的问题，接下来就是上脚本了：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nv">intern</span><span class="o">=</span><span class="k">$(</span>xrandr <span class="p">|</span> grep <span class="s2">" connected"</span> <span class="p">|</span> grep <span class="s2">"eDP"</span> <span class="p">|</span> cut -d<span class="s2">" "</span> -f1<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">resolution</span><span class="o">=</span><span class="s2">"2560x1600"</span>
</span></span><span class="line"><span class="cl"><span class="nv">low_mode</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">resolution</span><span class="si">}</span><span class="s2">_60.00"</span>
</span></span><span class="line"><span class="cl"><span class="nv">high_mode</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">resolution</span><span class="si">}</span><span class="s2">_165.00"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> add_mode <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">$(</span>xrandr <span class="p">|</span> grep -E -c <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="k">)</span><span class="s2">"</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$low_mode</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            xrandr --newmode <span class="nv">$low_mode</span> 282.7 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync
</span></span><span class="line"><span class="cl">            xrandr --addmode <span class="s2">"</span><span class="nv">$intern</span><span class="s2">"</span> <span class="nv">$low_mode</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$high_mode</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            xrandr --newmode <span class="nv">$high_mode</span> 777.410 <span class="m">2560</span> <span class="m">2608</span> <span class="m">2640</span> <span class="m">2720</span> <span class="m">1600</span> <span class="m">1603</span> <span class="m">1609</span> <span class="m">1732</span> -HSync -VSync
</span></span><span class="line"><span class="cl">            xrandr --addmode <span class="s2">"</span><span class="nv">$intern</span><span class="s2">"</span> <span class="nv">$high_mode</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> change_fps <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"low"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        add_mode <span class="s2">"</span><span class="nv">$low_mode</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl">        xrandr --output <span class="s2">"</span><span class="nv">$intern</span><span class="s2">"</span> --mode <span class="nv">$low_mode</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"high"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        add_mode <span class="s2">"</span><span class="nv">$high_mode</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl">        xrandr --output <span class="s2">"</span><span class="nv">$intern</span><span class="s2">"</span> --mode <span class="nv">$high_mode</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$#</span><span class="s2">"</span> -eq <span class="m">0</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-h"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">"Usage: </span><span class="nv">$0</span><span class="s2"> [low|high]"</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">change_fps <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span></span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-04-25T02:40:04+08:00
</span></span><span class="line"><span class="cl">update@2023-05-08T05:52:52+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/76</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>记一次莫名的BTRFS修复过程</title>
      <link>https://blog.ferstar.org/posts/btrfs-filesystem-corruption-repair/</link>
      <pubDate>Tue, 07 Feb 2023 06:32:08 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/btrfs-filesystem-corruption-repair/</guid>
      <description>吃着火锅唱着歌，啪，分区被锁，dmesg 日志飘红 —— 盘挂了&#xA;赶紧重启进 LiveCD 尝试挂载mount -o rescue=usebackuproot /dev/nvme0n1p5 /mnt，扑街&#xA;[ 183.966960] BTRFS info (device nvme0n1p5): using crc32c (crc32c-intel) checksum algorithm [ 183.966967] BTRFS info (device nvme0n1p5): trying to use backup root at mount time [ 183.966968] BTRFS info (device nvme0n1p5): using free space tree [ 183.968909] BTRFS info (device nvme0n1p5): bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 1, gen 0 [ 183.985663] BTRFS info (device nvme0n1p5): enabling ssd optimizations [ 183.985665] BTRFS info (device nvme0n1p5): start tree-log replay [ 184.107729] BTRFS error (device nvme0n1p5): incorrect extent count for 122436976640; counted 1577, expected 1575 [ 184.107744] BTRFS: error (device nvme0n1p5) in btrfs_replay_log:2395: errno=-5 IO failure (Failed to recover log tree) [ 184.120547] BTRFS error (device nvme0n1p5: state E): open_ctree failed 死马当活马医，btrfsck --repair /dev/nvme0n1p5 有个倒计时警告，不管了，直接上&#xA;</description><content:encoded><![CDATA[<blockquote><p>吃着火锅唱着歌，啪，分区被锁，dmesg 日志飘红 —— 盘挂了</p>
</blockquote><p>赶紧重启进 LiveCD 尝试挂载<code>mount -o rescue=usebackuproot /dev/nvme0n1p5 /mnt</code>，扑街</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="o">[</span>  183.966960<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: using crc32c <span class="o">(</span>crc32c-intel<span class="o">)</span> checksum algorithm
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.966967<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: trying to use backup root at mount <span class="nb">time</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.966968<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: using free space tree
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.968909<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 1, gen <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.985663<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: enabling ssd optimizations
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.985665<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: start tree-log replay
</span></span><span class="line"><span class="cl"><span class="o">[</span>  184.107729<span class="o">]</span> BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: incorrect extent count <span class="k">for</span> 122436976640<span class="p">;</span> counted 1577, expected <span class="m">1575</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>  184.107744<span class="o">]</span> BTRFS: error <span class="o">(</span>device nvme0n1p5<span class="o">)</span> in btrfs_replay_log:2395: <span class="nv">errno</span><span class="o">=</span>-5 IO failure <span class="o">(</span>Failed to recover log tree<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>  184.120547<span class="o">]</span> BTRFS error <span class="o">(</span>device nvme0n1p5: state E<span class="o">)</span>: open_ctree failed</span></span></code></pre></div></div>
<p>死马当活马医，<code>btrfsck --repair /dev/nvme0n1p5</code> 有个倒计时警告，不管了，直接上</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">enabling repair mode
</span></span><span class="line"><span class="cl">WARNING:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        Do not use --repair unless you are advised to <span class="k">do</span> so by a developer
</span></span><span class="line"><span class="cl">        or an experienced user, and <span class="k">then</span> only after having accepted that no
</span></span><span class="line"><span class="cl">        fsck can successfully repair all types of filesystem corruption. Eg.
</span></span><span class="line"><span class="cl">        some software or hardware bugs can fatally damage a volume.
</span></span><span class="line"><span class="cl">        The operation will start in <span class="m">10</span> seconds.
</span></span><span class="line"><span class="cl">        Use Ctrl-C to stop it.
</span></span><span class="line"><span class="cl"><span class="m">10</span> <span class="m">9</span> <span class="m">8</span> <span class="m">7</span> <span class="m">6</span> <span class="m">5</span> <span class="m">4</span> <span class="m">3</span> <span class="m">2</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">Starting repair.
</span></span><span class="line"><span class="cl">Opening filesystem to check...
</span></span><span class="line"><span class="cl">Checking filesystem on /dev/nvme0n1p5
</span></span><span class="line"><span class="cl">UUID: 3681c364-deb7-4275-8e5d-9c6991467acb
</span></span><span class="line"><span class="cl">repair mode will force to clear out log tree, are you sure? <span class="o">[</span>y/N<span class="o">]</span>: y
</span></span><span class="line"><span class="cl"><span class="o">[</span>1/7<span class="o">]</span> checking root items
</span></span><span class="line"><span class="cl">Fixed <span class="m">0</span> roots.
</span></span><span class="line"><span class="cl"><span class="o">[</span>2/7<span class="o">]</span> checking extents
</span></span><span class="line"><span class="cl">data extent<span class="o">[</span>124231548928, 61440<span class="o">]</span> referencer count mismatch <span class="o">(</span>parent 180895744<span class="o">)</span> wanted <span class="m">0</span> have <span class="m">1</span>
</span></span><span class="line"><span class="cl">data extent<span class="o">[</span>124231548928, 61440<span class="o">]</span> bytenr mimsmatch, extent item bytenr <span class="m">124231548928</span> file item bytenr <span class="m">0</span>
</span></span><span class="line"><span class="cl">data extent<span class="o">[</span>124231548928, 61440<span class="o">]</span> referencer count mismatch <span class="o">(</span>parent 4503599808266240<span class="o">)</span> wanted <span class="m">1</span> have <span class="m">0</span>
</span></span><span class="line"><span class="cl">backpointer mismatch on <span class="o">[</span><span class="m">124231548928</span> 61440<span class="o">]</span>
</span></span><span class="line"><span class="cl">repair deleting extent record: key <span class="o">[</span>124231548928,168,61440<span class="o">]</span>
</span></span><span class="line"><span class="cl">adding new data backref on <span class="m">124231548928</span> parent <span class="m">180895744</span> owner <span class="m">0</span> offset <span class="m">0</span> found <span class="m">1</span>
</span></span><span class="line"><span class="cl">Repaired extent references <span class="k">for</span> <span class="m">124231548928</span>
</span></span><span class="line"><span class="cl">super bytes used <span class="m">112946282496</span> mismatches actual used <span class="m">112946118656</span>
</span></span><span class="line"><span class="cl">No device size related problem found
</span></span><span class="line"><span class="cl"><span class="o">[</span>3/7<span class="o">]</span> checking free space tree
</span></span><span class="line"><span class="cl">free space info recorded <span class="m">1575</span> extents, counted <span class="m">1577</span>
</span></span><span class="line"><span class="cl">There are still entries left in the space cache
</span></span><span class="line"><span class="cl">cache appears valid but isn<span class="err">'</span>t <span class="m">122436976640</span>
</span></span><span class="line"><span class="cl">Clear free space cache v2
</span></span><span class="line"><span class="cl">free space cache v2 cleared
</span></span><span class="line"><span class="cl"><span class="o">[</span>4/7<span class="o">]</span> checking fs roots
</span></span><span class="line"><span class="cl"><span class="o">[</span>5/7<span class="o">]</span> checking only csums items <span class="o">(</span>without verifying data<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>6/7<span class="o">]</span> checking root refs
</span></span><span class="line"><span class="cl"><span class="o">[</span>7/7<span class="o">]</span> checking quota groups skipped <span class="o">(</span>not enabled on this FS<span class="o">)</span>
</span></span><span class="line"><span class="cl">found <span class="m">225892401152</span> bytes used, no error found
</span></span><span class="line"><span class="cl">total csum bytes: <span class="m">217257440</span>
</span></span><span class="line"><span class="cl">total tree bytes: <span class="m">3289513984</span>
</span></span><span class="line"><span class="cl">total fs tree bytes: <span class="m">2747269120</span>
</span></span><span class="line"><span class="cl">total extent tree bytes: <span class="m">271155200</span>
</span></span><span class="line"><span class="cl">btree space waste bytes: <span class="m">533066137</span>
</span></span><span class="line"><span class="cl">file data blocks allocated: <span class="m">2738343133184</span>
</span></span><span class="line"><span class="cl"> referenced <span class="m">315154259968</span></span></span></code></pre></div></div>
<p>估摸着有戏，赶紧再挂一次<code>mount -o rescue=usebackuproot /dev/nvme0n1p5 /mnt</code>，居然就成了，赶紧<code>dmesg | grep -i btrfs</code>瞧一瞧</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="o">[</span>  183.966960<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: using crc32c <span class="o">(</span>crc32c-intel<span class="o">)</span> checksum algorithm
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.966967<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: trying to use backup root at mount <span class="nb">time</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.966968<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: using free space tree
</span></span><span class="line"><span class="cl"><span class="o">[</span>  183.968909<span class="o">]</span> BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 1, gen <span class="m">0</span></span></span></code></pre></div></div>
<p>这时候分区已经正常了，但是这行数字看着扎眼，强迫症不能忍：<code> errs: wr 0, rd 0, flush 0, corrupt 1, gen 0</code></p>
<p>再来一发<code>btrfs scrub start /mnt</code>，开<code>dmesg -w</code>看下哪些文档没得救</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: scrub: started on devid <span class="m">1</span>
</span></span><span class="line"><span class="cl">BTRFS warning <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: checksum error at logical <span class="m">54326345728</span> on dev /dev/nvme0n1p5, physical 56482217984, root 259, inode 611588, offset 1441792, length 4096, links <span class="m">1</span> <span class="o">(</span>path: ferstar/Downloads/log <span class="o">(</span>2<span class="o">)</span>.tar.gz<span class="o">)</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 2, gen <span class="m">0</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: unable to fixup <span class="o">(</span>regular<span class="o">)</span> error at logical <span class="m">54326345728</span> on dev /dev/nvme0n1p5
</span></span><span class="line"><span class="cl">BTRFS warning <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: checksum error at logical <span class="m">123690389504</span> on dev /dev/nvme0n1p5, physical 125846261760, root 259, inode 2161195, offset 593920, length 4096, links <span class="m">1</span> <span class="o">(</span>path: ferstar/pyprojects/hkex/data/files/cf/0cdb845691cbe2c22789c727e00745<span class="o">)</span>
</span></span><span class="line"><span class="cl">BTRFS warning <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: checksum error at logical <span class="m">123690455040</span> on dev /dev/nvme0n1p5, physical 125846327296, root 259, inode 2161195, offset 659456, length 4096, links <span class="m">1</span> <span class="o">(</span>path: ferstar/pyprojects/hkex/data/files/cf/0cdb845691cbe2c22789c727e00745<span class="o">)</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 3, gen <span class="m">0</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 4, gen <span class="m">0</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: unable to fixup <span class="o">(</span>regular<span class="o">)</span> error at logical <span class="m">123690455040</span> on dev /dev/nvme0n1p5
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: unable to fixup <span class="o">(</span>regular<span class="o">)</span> error at logical <span class="m">123690389504</span> on dev /dev/nvme0n1p5
</span></span><span class="line"><span class="cl">BTRFS warning <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: checksum error at logical <span class="m">123690463232</span> on dev /dev/nvme0n1p5, physical 125846335488, root 259, inode 2161195, offset 667648, length 4096, links <span class="m">1</span> <span class="o">(</span>path: ferstar/pyprojects/hkex/data/files/cf/0cdb845691cbe2c22789c727e00745<span class="o">)</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: bdev /dev/nvme0n1p5 errs: wr 0, rd 0, flush 0, corrupt 5, gen <span class="m">0</span>
</span></span><span class="line"><span class="cl">BTRFS error <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: unable to fixup <span class="o">(</span>regular<span class="o">)</span> error at logical <span class="m">123690463232</span> on dev /dev/nvme0n1p5
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: scrub: finished on devid <span class="m">1</span> with status: <span class="m">0</span></span></span></code></pre></div></div>
<p>把没救的文档删掉，然后清一下错误计数器<code>btrfs dev stats -z /mnt</code></p>
<p>卸载再挂载一次，日志终于正常，完美</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Btrfs loaded, <span class="nv">crc32c</span><span class="o">=</span>crc32c-intel, <span class="nv">zoned</span><span class="o">=</span>yes, <span class="nv">fsverity</span><span class="o">=</span>yes
</span></span><span class="line"><span class="cl">BTRFS: device fsid 3681c364-deb7-4275-8e5d-9c6991467acb devid <span class="m">1</span> transid <span class="m">104541</span> /dev/nvme0n1p5 scanned by systemd-udevd <span class="o">(</span>270<span class="o">)</span>
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: using crc32c <span class="o">(</span>crc32c-intel<span class="o">)</span> checksum algorithm
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: disk space caching is enabled
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: enabling ssd optimizations
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5<span class="o">)</span>: checking UUID tree
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5: state M<span class="o">)</span>: trying to use backup root at mount <span class="nb">time</span>
</span></span><span class="line"><span class="cl">BTRFS info <span class="o">(</span>device nvme0n1p5: state M<span class="o">)</span>: force zstd compression, level <span class="m">3</span></span></span></code></pre></div></div>
<p>附上参考资料：</p>
<ul>
<li><a href="https://zyyme.com/btrfs-fix.html"  target="_blank" rel="noreferrer">https://zyyme.com/btrfs-fix.html</a></li>
<li><a href="https://btrfs.readthedocs.io"  target="_blank" rel="noreferrer">https://btrfs.readthedocs.io</a></li>
<li><a href="https://blog.firerain.me/article/21"  target="_blank" rel="noreferrer">https://blog.firerain.me/article/21</a></li>
<li><a href="https://www.reddit.com/r/btrfs/comments/plmm01/comment/hcc83el"  target="_blank" rel="noreferrer">https://www.reddit.com/r/btrfs/comments/plmm01/comment/hcc83el</a></li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-02-07T06:32:08+08:00
</span></span><span class="line"><span class="cl">update@2023-02-07T06:32:15+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/75</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Upgrading PostgreSQL from v14 to v15</title>
      <link>https://blog.ferstar.org/posts/upgrade-postgresql-14-15/</link>
      <pubDate>Fri, 03 Feb 2023 11:51:35 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/upgrade-postgresql-14-15/</guid>
      <description>奶奶的又升16了，都开始刷版本，我不跟了，Docker 钉死先&#xA;version: &amp;#34;3.8&amp;#34; services: postgres: restart: on-failure container_name: pg_pin image: postgres:15 volumes: - &amp;#34;./pg_data:/var/lib/postgresql/data&amp;#34; environment: - DEBUG=false - POSTGRES_PASSWORD=xzsDwlk3LqaY # ports: # - 5432:5432 network_mode: host https://wiki.archlinux.org/title/PostgreSQL#Upgrading_PostgreSQL&#xA;我的 PG 一般是随项目走的，假设路径：data/pg_data&#xA;</description><content:encoded><![CDATA[<p>奶奶的又升16了，都开始刷版本，我不跟了，Docker 钉死先</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.8"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">postgres</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="kc">on</span>-<span class="l">failure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pg_pin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:15</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">"./pg_data:/var/lib/postgresql/data"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DEBUG=false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_PASSWORD=xzsDwlk3LqaY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ports:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># - 5432:5432</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host</span></span></span></code></pre></div></div>
<hr>
<blockquote><p><a href="https://wiki.archlinux.org/title/PostgreSQL#Upgrading_PostgreSQL"  target="_blank" rel="noreferrer">https://wiki.archlinux.org/title/PostgreSQL#Upgrading_PostgreSQL</a></p>
</blockquote><p>我的 PG 一般是随项目走的，假设路径：data/pg_data</p>
<ul>
<li>mv data/pg_data data/pg_data.old</li>
<li>pacman -S postgresql-old-upgrade</li>
<li>initdb -D data/pg_data –locale=zh_CN.UTF-8 –encoding=UTF8</li>
<li>pg_upgrade -b /opt/pgsql-14/bin -B /usr/bin -d data/pg_data.old -D data/pg_data</li>
<li>一切正常后删掉 data/pg_data.old 备份</li>
<li>卸载掉 postgresql-old-upgrade</li>
</ul>
<p>一些输出：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The files belonging to this database system will be owned by user <span class="s2">"ferstar"</span>.
</span></span><span class="line"><span class="cl">This user must also own the server process.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The database cluster will be initialized with locale <span class="s2">"zh_CN.UTF-8"</span>.
</span></span><span class="line"><span class="cl">initdb: could not find suitable text search configuration <span class="k">for</span> locale <span class="s2">"zh_CN.UTF-8"</span>
</span></span><span class="line"><span class="cl">The default text search configuration will be <span class="nb">set</span> to <span class="s2">"simple"</span>.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Data page checksums are disabled.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">creating directory data/pg_data ... ok
</span></span><span class="line"><span class="cl">creating subdirectories ... ok
</span></span><span class="line"><span class="cl">selecting dynamic shared memory implementation ... posix
</span></span><span class="line"><span class="cl">selecting default max_connections ... <span class="m">100</span>
</span></span><span class="line"><span class="cl">selecting default shared_buffers ... 128MB
</span></span><span class="line"><span class="cl">selecting default <span class="nb">time</span> zone ... Asia/Shanghai
</span></span><span class="line"><span class="cl">creating configuration files ... ok
</span></span><span class="line"><span class="cl">running bootstrap script ... ok
</span></span><span class="line"><span class="cl">performing post-bootstrap initialization ... ok
</span></span><span class="line"><span class="cl">syncing data to disk ... ok
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">initdb: warning: enabling <span class="s2">"trust"</span> authentication <span class="k">for</span> <span class="nb">local</span> connections
</span></span><span class="line"><span class="cl">initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next <span class="nb">time</span> you run initdb.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Success. You can now start the database server using:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    pg_ctl -D data/pg_data -l logfile start
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Performing Consistency Checks
</span></span><span class="line"><span class="cl">-----------------------------
</span></span><span class="line"><span class="cl">Checking cluster versions                                   ok
</span></span><span class="line"><span class="cl">Checking database user is the install user                  ok
</span></span><span class="line"><span class="cl">Checking database connection settings                       ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> prepared transactions                          ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> system-defined composite types in user tables  ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> reg* data types in user tables                 ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> contrib/isn with bigint-passing mismatch       ok
</span></span><span class="line"><span class="cl">Creating dump of global objects                             ok
</span></span><span class="line"><span class="cl">Creating dump of database schemas
</span></span><span class="line"><span class="cl">                                                            ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> presence of required libraries                 ok
</span></span><span class="line"><span class="cl">Checking database user is the install user                  ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> prepared transactions                          ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> new cluster tablespace directories             ok
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If pg_upgrade fails after this point, you must re-initdb the
</span></span><span class="line"><span class="cl">new cluster before continuing.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Performing Upgrade
</span></span><span class="line"><span class="cl">------------------
</span></span><span class="line"><span class="cl">Analyzing all rows in the new cluster                       ok
</span></span><span class="line"><span class="cl">Freezing all rows in the new cluster                        ok
</span></span><span class="line"><span class="cl">Deleting files from new pg_xact                             ok
</span></span><span class="line"><span class="cl">Copying old pg_xact to new server                           ok
</span></span><span class="line"><span class="cl">Setting oldest XID <span class="k">for</span> new cluster                          ok
</span></span><span class="line"><span class="cl">Setting next transaction ID and epoch <span class="k">for</span> new cluster       ok
</span></span><span class="line"><span class="cl">Deleting files from new pg_multixact/offsets                ok
</span></span><span class="line"><span class="cl">Copying old pg_multixact/offsets to new server              ok
</span></span><span class="line"><span class="cl">Deleting files from new pg_multixact/members                ok
</span></span><span class="line"><span class="cl">Copying old pg_multixact/members to new server              ok
</span></span><span class="line"><span class="cl">Setting next multixact ID and offset <span class="k">for</span> new cluster        ok
</span></span><span class="line"><span class="cl">Resetting WAL archives                                      ok
</span></span><span class="line"><span class="cl">Setting frozenxid and minmxid counters in new cluster       ok
</span></span><span class="line"><span class="cl">Restoring global objects in the new cluster                 ok
</span></span><span class="line"><span class="cl">Restoring database schemas in the new cluster
</span></span><span class="line"><span class="cl">                                                            ok
</span></span><span class="line"><span class="cl">Copying user relation files
</span></span><span class="line"><span class="cl">                                                            ok
</span></span><span class="line"><span class="cl">Setting next OID <span class="k">for</span> new cluster                            ok
</span></span><span class="line"><span class="cl">Sync data directory to disk                                 ok
</span></span><span class="line"><span class="cl">Creating script to delete old cluster                       ok
</span></span><span class="line"><span class="cl">Checking <span class="k">for</span> extension updates                              ok
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Upgrade Complete
</span></span><span class="line"><span class="cl">----------------
</span></span><span class="line"><span class="cl">Optimizer statistics are not transferred by pg_upgrade.
</span></span><span class="line"><span class="cl">Once you start the new server, consider running:
</span></span><span class="line"><span class="cl">    /usr/bin/vacuumdb --all --analyze-in-stages
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Running this script will delete the old cluster<span class="err">'</span>s data files:
</span></span><span class="line"><span class="cl">    ./delete_old_cluster.sh</span></span></code></pre></div></div>
<p>可能会碰到的报错：</p>
<p>Q: lc_collate values for database “template1” do not match:  old “zh_CN.UTF-8”, new “en_US.UTF-8”</p>
<p>A: initdb -D data/pg_data –locale=zh_CN.UTF-8 –encoding=UTF8 指定一样的locale即可</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-02-03T11:51:35+08:00
</span></span><span class="line"><span class="cl">update@2023-12-26T02:27:10+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/74</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Linux触控板手势增强之「三指拖拽」</title>
      <link>https://blog.ferstar.org/posts/linux-touchpad-gestures-drag/</link>
      <pubDate>Sun, 29 Jan 2023 02:08:34 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/linux-touchpad-gestures-drag/</guid>
      <description>在 Linux 上找回 macOS 丝滑的“三指拖拽”体验！基于 Rust 实现的高性能 libinput 手势增强方案，支持 X11 与 Wayland，CPU 占用不到 1% 且配置灵活。</description><content:encoded><![CDATA[<p>转用 Linux 后一直都比较怀念 macOS 上丝滑的<strong>三指拖拽</strong>效果，鉴于近几年出的 Windows 本子触控板面积以及跟手性肉眼可见的改善了很多，我觉得是时候在 Linux 上折腾下<strong>触控板手势</strong>了。</p>
<p>After switching to Linux, I’ve been missing the smooth <strong>three-finger drag</strong> experience from macOS. Given that recent Windows laptops have significantly improved touchpad size and responsiveness, I decided it was time to tackle <strong>touchpad gestures</strong> on Linux.</p>
<hr>

<h2 class="relative group">调研与选型 / Research and Selection
    <div id="调研与选型--research-and-selection" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b0%83%e7%a0%94%e4%b8%8e%e9%80%89%e5%9e%8b--research-and-selection" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>基本上就是两种实现思路：</p>
<ol>
<li>解析 <code>libinput debug-events</code> 输出，判断手势进而借用类似 <code>xdotool</code> 之类的工具发送具体的键盘组合或者鼠标点击或位移指令。</li>
<li>直接调用 <code>libinput</code> API，明显此方法性能最优。</li>
</ol>
<hr>

<h2 class="relative group">实现方案 / Implementation Approach
    <div id="实现方案--implementation-approach" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9e%e7%8e%b0%e6%96%b9%e6%a1%88--implementation-approach" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>我选择了一个 Rust <a href="https://github.com/riley-martin/gestures"  target="_blank" rel="noreferrer">实现</a>开抄，负责实现 API 级别的触控手势识别，然后缝合了另一个 Rust <a href="https://github.com/marsqing/libinput-three-finger-drag"  target="_blank" rel="noreferrer">实现</a>，负责实现 API 级别的<strong>拖拽</strong>效果。</p>

<h3 class="relative group">架构逻辑 / Architectural Logic
    <div id="架构逻辑--architectural-logic" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9e%b6%e6%9e%84%e9%80%bb%e8%be%91--architectural-logic" aria-label="锚点">#</a>
    </span>
    
</h3>
<pre class="not-prose mermaid">graph TD
    A[Touchpad Event] --> B{libinput}
    B --> C[ferstar/gestures <br/>Rust Engine]
    C --> D{Display Server}
    D -- X11 --> E[libxdo API]
    D -- Wayland --> F[ydotool daemon]
    E --> G[Smooth Drag / Key Stroke]
    F --> G
    
    style C fill:#f96,stroke:#333,stroke-width:2px
    style G fill:#4ecdc4,stroke:#333,stroke-width:2px</pre>
<p>目前项目已发展到 <strong>v0.8.1</strong> 版本，主要改进包括：</p>
<ul>
<li><strong>双平台支持</strong>：同时支持 X11 和 Wayland（自动检测）</li>
<li><strong>性能优化</strong>：
<ul>
<li>X11 直接使用 libxdo API，延迟最小</li>
<li>Wayland 优化 ydotool 集成，60 FPS 节流</li>
<li>4 线程池防止 PID 耗尽</li>
<li>正则缓存（once_cell::Lazy）</li>
<li>事件缓存（1秒）减少配置查找</li>
</ul>
</li>
</ul>
<hr>

<h2 class="relative group">性能表现 / Performance Metrics
    <div id="性能表现--performance-metrics" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%a7%e8%83%bd%e8%a1%a8%e7%8e%b0--performance-metrics" aria-label="锚点">#</a>
    </span>
    
</h2>
<ol>
<li>
<p><strong>CPU 占用极低</strong></p>
<ul>
<li>极限情况：疯狂三指拖拽某窗口，本 fork 实现 CPU 占用不到 1%</li>
<li>原实现 5~10%</li>
<li>Python、Ruby 等实现 20%+</li>
</ul>
</li>
<li>
<p><strong>资源占用</strong></p>
<ul>
<li>内存占用不到 5MB</li>
<li>程序体积不到 2MB</li>
<li>无多余依赖</li>
</ul>
</li>
</ol>
<hr>

<h2 class="relative group">安装使用 / Installation & Usage
    <div id="安装使用--installation--usage" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e4%bd%bf%e7%94%a8--installation--usage" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">依赖安装 / Install Dependencies
    <div id="依赖安装--install-dependencies" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%be%9d%e8%b5%96%e5%ae%89%e8%a3%85--install-dependencies" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong>Ubuntu/Debian:</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install libudev-dev libinput-dev libxdo-dev xdotool
</span></span><span class="line"><span class="cl"><span class="c1"># Wayland 需要额外安装</span>
</span></span><span class="line"><span class="cl">sudo apt install ydotool</span></span></code></pre></div></div>
<p><strong>Arch Linux:</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S libinput xdotool
</span></span><span class="line"><span class="cl"><span class="c1"># Wayland</span>
</span></span><span class="line"><span class="cl">yay -S ydotool</span></span></code></pre></div></div>

<h3 class="relative group">安装程序 / Install Binary
    <div id="安装程序--install-binary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%89%e8%a3%85%e7%a8%8b%e5%ba%8f--install-binary" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><strong>方法 1：直接下载预编译二进制</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 从 Releases 下载最新版本</span>
</span></span><span class="line"><span class="cl">wget https://github.com/ferstar/gestures/releases/latest/download/gestures
</span></span><span class="line"><span class="cl">chmod +x gestures
</span></span><span class="line"><span class="cl">sudo mv gestures /usr/local/bin/</span></span></code></pre></div></div>
<p><strong>方法 2：Cargo 安装</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cargo install --git https://github.com/ferstar/gestures.git</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">配置说明 / Configuration
    <div id="配置说明--configuration" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%85%8d%e7%bd%ae%e8%af%b4%e6%98%8e--configuration" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>配置文件使用 KDL 格式，示例：</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-kdl" data-lang="kdl">// 三指拖拽（X11 & Wayland 通用）
gesture "drag" swipe any {
    fingers 3
    acceleration 1.0      // 拖拽速度
    mouse_up_delay 500    // 抬手后延迟（ms）
}

// 四指上滑切换工作区
gesture "switch-workspace-up" swipe up {
    fingers 4
    exec "xdotool" "key" "super+Page_Up"
}</code></pre></div>
<hr>

<h2 class="relative group">运行程序 / Running the Program
    <div id="运行程序--running-the-program" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%bf%90%e8%a1%8c%e7%a8%8b%e5%ba%8f--running-the-program" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">Systemd 服务（推荐）/ Systemd Service (Recommended)
    <div id="systemd-服务推荐-systemd-service-recommended" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#systemd-%e6%9c%8d%e5%8a%a1%e6%8e%a8%e8%8d%90-systemd-service-recommended" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 安装服务文件</span>
</span></span><span class="line"><span class="cl">gestures install-service
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 启动并设为开机自启</span>
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now gestures</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">常见问题 / Common Issues
    <div id="常见问题--common-issues" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98--common-issues" aria-label="锚点">#</a>
    </span>
    
</h2>

<h3 class="relative group">权限问题 / Permission Issues
    <div id="权限问题--permission-issues" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9d%83%e9%99%90%e9%97%ae%e9%a2%98--permission-issues" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>需要将用户加入 <code>input</code> 组：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo usermod -aG input <span class="nv">$USER</span></span></span></code></pre></div></div>
<hr>

<h2 class="relative group">项目链接 / Project Links
    <div id="项目链接--project-links" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%a1%b9%e7%9b%ae%e9%93%be%e6%8e%a5--project-links" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li><strong>GitHub</strong>: <a href="https://github.com/ferstar/gestures"  target="_blank" rel="noreferrer">https://github.com/ferstar/gestures</a></li>
<li><strong>最新发布</strong>: <a href="https://github.com/ferstar/gestures/releases"  target="_blank" rel="noreferrer">https://github.com/ferstar/gestures/releases</a></li>
<li><strong>问题反馈</strong>: <a href="https://github.com/ferstar/gestures/issues"  target="_blank" rel="noreferrer">https://github.com/ferstar/gestures/issues</a></li>
</ul>
<hr>
<p><strong>Enjoy!</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">29</span><span class="nx">T02</span><span class="o">:</span><span class="mi">08</span><span class="o">:</span><span class="mi">34</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">04</span><span class="nx">T06</span><span class="o">:</span><span class="mi">00</span><span class="o">:</span><span class="mi">00</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">comment</span><span class="err">@</span><span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/73
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Do not use Super in comprehension if you need cython</title>
      <link>https://blog.ferstar.org/posts/cython-comprehension-super-bug/</link>
      <pubDate>Tue, 24 Jan 2023 01:24:35 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/cython-comprehension-super-bug/</guid>
      <description>一般项目交付给客户都会把源码打包一下，通常是下面的姿势：&#xA;# py to c cython -X language_level=3 -X annotation_typing=False --directive always_allow_keywords=true debug.py # c to so gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing $(python3-config --includes) -o debug.so debug.c 你会发现源码能行的一些写法，编译以后执行就会报莫名其妙的错误，比如：Super in comprehension&#xA;</description><content:encoded><![CDATA[<p>一般项目交付给客户都会把源码打包一下，通常是下面的姿势：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># py to c</span>
</span></span><span class="line"><span class="cl">cython -X <span class="nv">language_level</span><span class="o">=</span><span class="m">3</span> -X <span class="nv">annotation_typing</span><span class="o">=</span>False --directive <span class="nv">always_allow_keywords</span><span class="o">=</span><span class="nb">true</span> debug.py
</span></span><span class="line"><span class="cl"><span class="c1"># c to so</span>
</span></span><span class="line"><span class="cl">gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing <span class="k">$(</span>python3-config --includes<span class="k">)</span> -o debug.so debug.c</span></span></code></pre></div></div>
<p>你会发现源码能行的一些写法，编译以后执行就会报莫名其妙的错误，比如：<code>Super in comprehension</code></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">A</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">hi</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">B</span><span class="p">(</span><span class="n">A</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@property</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">wow</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">hi</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">B</span><span class="p">()</span><span class="o">.</span><span class="n">wow</span><span class="p">)</span></span></span></code></pre></div></div>
<p>这段代码源码运行没有任何问题，执行<code>B().wow</code>输出就是<code>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</code>。但是编译后再运行就会报如下的错：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Python 3.11.6 <span class="o">(</span>main, Nov <span class="m">14</span> 2023, 09:36:21<span class="o">)</span> <span class="o">[</span>GCC 13.2.1 20230801<span class="o">]</span> on linux
</span></span><span class="line"><span class="cl">Type <span class="s2">"help"</span>, <span class="s2">"copyright"</span>, <span class="s2">"credits"</span> or <span class="s2">"license"</span> <span class="k">for</span> more information.
</span></span><span class="line"><span class="cl">>>> import debug
</span></span><span class="line"><span class="cl">>>> debug.B<span class="o">()</span>.wow
</span></span><span class="line"><span class="cl">Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
</span></span><span class="line"><span class="cl">  File <span class="s2">"<stdin>"</span>, line 1, in <module>
</span></span><span class="line"><span class="cl">  File <span class="s2">"debug.py"</span>, line 10, in debug.B.wow
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">RuntimeError: super<span class="o">()</span>: no arguments</span></span></code></pre></div></div>
<p>不用担心，这不是你的问题，而是 cython 一个上古的 bug：https://github.com/cython/cython/issues/1828</p>
<p>那团队协作中如何提示组员避免这种写法呢？我们可以借助 <code>pylint</code> 来检查&提示：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">astroid</span> <span class="kn">import</span> <span class="n">nodes</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pylint</span> <span class="kn">import</span> <span class="n">checkers</span><span class="p">,</span> <span class="n">interfaces</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pylint.checkers</span> <span class="kn">import</span> <span class="n">utils</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SupperInCompChecker</span><span class="p">(</span><span class="n">checkers</span><span class="o">.</span><span class="n">BaseChecker</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">__implements__</span> <span class="o">=</span> <span class="p">(</span><span class="n">interfaces</span><span class="o">.</span><span class="n">IAstroidChecker</span><span class="p">,)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="s2">"super-in-comprehension"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">msgs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"R9527"</span><span class="p">:</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"You need to abstract super() to a variable"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"do-not-use-super-in-comprehension"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">"Use super() in comprehension may cause compatibility issues with cython."</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">options</span> <span class="o">=</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">priority</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@utils.only_required_for_messages</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"do-not-use-super-in-comprehension"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">visit_comprehension</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">:</span> <span class="n">nodes</span><span class="o">.</span><span class="n">Comprehension</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_check_super_in_comprehension</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_check_super_in_comprehension</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">:</span> <span class="n">nodes</span><span class="o">.</span><span class="n">Comprehension</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="s2">" super("</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">node</span><span class="o">.</span><span class="n">as_string</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">add_message</span><span class="p">(</span><span class="s2">"do-not-use-super-in-comprehension"</span><span class="p">,</span> <span class="n">node</span><span class="o">=</span><span class="n">node</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="n">linter</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">linter</span><span class="o">.</span><span class="n">register_checker</span><span class="p">(</span><span class="n">SupperInCompChecker</span><span class="p">(</span><span class="n">linter</span><span class="p">))</span></span></span></code></pre></div></div>
<p>至于这个checker怎么用，卖个关子，有心人自己去找：</p>
<p><a href="https://pylint.pycqa.org/en/latest/development_guide/how_tos/custom_checkers.html"  target="_blank" rel="noreferrer">https://pylint.pycqa.org/en/latest/development_guide/how_tos/custom_checkers.html</a></p>
<p>最终的效果大概是：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">You need to abstract super<span class="o">()</span> to a variable,
</span></span><span class="line"><span class="cl"><span class="k">do</span> not use super in comprehension...</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-01-24T01:24:35+08:00
</span></span><span class="line"><span class="cl">update@2023-12-27T09:08:09+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/72</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>gRPC小记</title>
      <link>https://blog.ferstar.org/posts/grpc-implementation-and-best-practices/</link>
      <pubDate>Tue, 24 Jan 2023 01:23:27 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/grpc-implementation-and-best-practices/</guid>
      <description>老早前给公司内部写了一个 gRPC 小框架叫 aipod，主要是做模型的搬运工，数据流向大概是这样：&#xA;model &amp;lt;--&amp;gt;[train/predict/log]&amp;lt;--&amp;gt; aipod &amp;lt;--&amp;gt; web app&#xA;本来是个小玩意，基本跑的还算愉快，但是随着业务发展，传输的数据到M再到G级别时，问题出现了，各种花式断连，不得不重构之，主要干了这么些事情：&#xA;</description><content:encoded><![CDATA[<p>老早前给公司内部写了一个 gRPC 小框架叫 aipod，主要是做模型的搬运工，数据流向大概是这样：</p>
<p><code>model <-->[train/predict/log]<--> aipod <--> web app</code></p>
<p>本来是个小玩意，基本跑的还算愉快，但是随着业务发展，传输的数据到M再到G级别时，问题出现了，各种花式断连，不得不重构之，主要干了这么些事情：</p>
<ol>
<li>改进默认配置</li>
<li>改变传输模式：从简单<code>Unary RPC</code>到<code>Bidirectional Streaming RPC</code></li>
<li>同步改异步，支持更高的并发</li>
<li>zstd 压缩</li>
<li>引入万能方法，解决多 aipod 实例无法被同 Nginx 分流&负载均衡的问题</li>
</ol>

<h3 class="relative group">改配置
    <div id="改配置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%94%b9%e9%85%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>实际部署中其实你根本不知道前面会套多少层类似 Nginx 的代理，任意一层超时链接就会被掐掉，所以这里的配置主要是为了降低不必要的断连而准备的，另外增加了超时重试的机制。我直接贴代码，就不解释了</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">json_config</span> <span class="o">=</span> json.dumps<span class="o">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># https://github.com/grpc/proposal/blob/master/A6-client-retries.md</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># https://docs.microsoft.com/en-us/aspnet/core/grpc/retries?view=aspnetcore-6.0#streaming-calls</span>
</span></span><span class="line"><span class="cl">    <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"methodConfig"</span>: <span class="o">[</span>
</span></span><span class="line"><span class="cl">            <span class="o">{</span>
</span></span><span class="line"><span class="cl">                <span class="s2">"name"</span>: <span class="o">[{</span><span class="s2">"service"</span>: <span class="s2">"ai.AI"</span><span class="o">}]</span>,
</span></span><span class="line"><span class="cl">                <span class="s2">"retryPolicy"</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">"maxAttempts"</span>: 5,
</span></span><span class="line"><span class="cl">                    <span class="s2">"initialBackoff"</span>: <span class="s2">"1s"</span>,
</span></span><span class="line"><span class="cl">                    <span class="s2">"maxBackoff"</span>: <span class="s2">"30s"</span>,
</span></span><span class="line"><span class="cl">                    <span class="s2">"backoffMultiplier"</span>: 2,
</span></span><span class="line"><span class="cl">                    <span class="s2">"retryableStatusCodes"</span>: <span class="o">[</span>
</span></span><span class="line"><span class="cl">                        grpc.StatusCode.INTERNAL.name,
</span></span><span class="line"><span class="cl">                        grpc.StatusCode.UNAVAILABLE.name,
</span></span><span class="line"><span class="cl">                        grpc.StatusCode.UNKNOWN.name,
</span></span><span class="line"><span class="cl">                    <span class="o">]</span>,
</span></span><span class="line"><span class="cl">                <span class="o">}</span>,
</span></span><span class="line"><span class="cl">                <span class="c1"># "retryThrottling":{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">#     "maxTokens": 10,</span>
</span></span><span class="line"><span class="cl">                <span class="c1">#     "tokenRatio": 0.1</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># }</span>
</span></span><span class="line"><span class="cl">            <span class="o">}</span>
</span></span><span class="line"><span class="cl">        <span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">COMMON_OPTIONS</span> <span class="o">=</span> <span class="o">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># default is -1, which is unlimited</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.max_send_message_length"</span>, -1<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.max_receive_message_length"</span>, -1<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.default_compression_algorithm"</span>, CompressionAlgorithm.gzip<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.grpc.default_compression_level"</span>, CompressionLevel.high<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># References:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># https://grpc.github.io/grpc/core/group__grpc__arg__keys.html</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># https://cs.mcgill.ca/~mxia3/2019/02/23/Using-gRPC-in-Production</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># https://gist.github.com/xiamx/6f5258511dc9180d3279adef4ffb212a</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># send keepalive ping every 5 second, default is 2 hours</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.keepalive_time_ms"</span>, 5000<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># keepalive ping time out after 120 seconds, default is 20 seconds</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.keepalive_timeout_ms"</span>, 120000<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># allow keepalive pings when there's no gRPC calls</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.keepalive_permit_without_calls"</span>, True<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># allow unlimited amount of keepalive pings without data</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.http2.max_pings_without_data"</span>, 0<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># allow grpc pings from client every 5 seconds</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.http2.min_time_between_pings_ms"</span>, 5000<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="c1"># allow grpc pings from client without data every 5 seconds</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.http2.min_ping_interval_without_data_ms"</span>, 5000<span class="o">)</span>,
</span></span><span class="line"><span class="cl"><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DEFAULT_CLIENT_OPTIONS</span> <span class="o">=</span> COMMON_OPTIONS + <span class="o">(</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.enable_retries"</span>, 1<span class="o">)</span>,
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.service_config"</span>, json_config<span class="o">)</span>,
</span></span><span class="line"><span class="cl"><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">DEFAULT_SERVER_OPTIONS</span> <span class="o">=</span> COMMON_OPTIONS + <span class="o">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 0 allows the server to accept any number of bad pings</span>
</span></span><span class="line"><span class="cl">    <span class="o">(</span><span class="s2">"grpc.http2.max_ping_strikes"</span>, 0<span class="o">)</span>,
</span></span><span class="line"><span class="cl"><span class="o">)</span></span></span></code></pre></div></div>

<h3 class="relative group">改传输模式
    <div id="改传输模式" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%94%b9%e4%bc%a0%e8%be%93%e6%a8%a1%e5%bc%8f" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>还是为了解决长阻塞任务被中断的问题，关于 Unary RPC 转 Bidirectional Streaming RPC 官方文档都有详细的解释，这里不赘述。本来正常的操作应该是想办法避免、降低长阻塞任务，尽量在超时范围内返回任务结果，但实际上你根本不知道调用方的任务是什么玩意，比如一个预测任务，抠搜客户只给配 CPU 那跑个大几十秒十几分钟都是稀松平常，完全无法控制，也根本无法定一个相对安全的超时时间。所以只能从自己身上下刀了：**分一个线程出来，专门负责给客户端响应心跳，另一个线程等待阻塞任务结束。**这样你甚至不用关心中间到底过多少 Nginx，每个节点的超时时间是多少，再低也不会低于1秒吧？只要下游真正写好异步任务，不阻塞 IO，那么就不会被掐掉链接。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">chaos</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request_iter</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">raw</span> <span class="o">=</span> <span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="sa">b</span><span class="s2">""</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">TemporaryDirectory</span><span class="p">()</span> <span class="k">as</span> <span class="n">tmp_dir</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_receive_stream</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">request_iter</span><span class="p">,</span> <span class="n">raw</span><span class="p">,</span> <span class="n">tmp_dir</span><span class="p">),</span>  <span class="c1"># 这个方法负责等待阻塞任务结束，收结果。</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_send_stream</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">raw</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@staticmethod</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">_send_stream</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">var</span><span class="p">:</span> <span class="n">Raw</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="ow">not</span> <span class="n">var</span><span class="o">.</span><span class="n">data</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 没结果就一直发心跳包给客户端</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">PONG</span><span class="o">.</span><span class="n">encode</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">var</span><span class="p">)</span></span></span></code></pre></div></div>

<h3 class="relative group">改异步
    <div id="改异步" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%94%b9%e5%bc%82%e6%ad%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这个没说的，别让 CPU 闲着嘛，多浪费。gRPC 官网有很好的示例代码，我顺路增加了 health check 以及 graceful shutdown 支持。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_configure_maintenance_server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">server</span><span class="p">:</span> <span class="n">grpc</span><span class="o">.</span><span class="n">Server</span><span class="p">,</span> <span class="n">address</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-></span> <span class="n">health</span><span class="o">.</span><span class="n">HealthServicer</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">server</span><span class="o">.</span><span class="n">add_insecure_port</span><span class="p">(</span><span class="n">address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Create a health check servicer. We use the non-blocking implementation</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># to avoid thread starvation.</span>
</span></span><span class="line"><span class="cl">    <span class="n">health_servicer</span> <span class="o">=</span> <span class="n">health</span><span class="o">.</span><span class="n">HealthServicer</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create a tuple of all the services we want to export via reflection.</span>
</span></span><span class="line"><span class="cl">    <span class="n">services</span> <span class="o">=</span> <span class="nb">tuple</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">service</span><span class="o">.</span><span class="n">full_name</span> <span class="k">for</span> <span class="n">service</span> <span class="ow">in</span> <span class="n">ai_pb2</span><span class="o">.</span><span class="n">DESCRIPTOR</span><span class="o">.</span><span class="n">services_by_name</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">reflection</span><span class="o">.</span><span class="n">SERVICE_NAME</span><span class="p">,</span> <span class="n">health</span><span class="o">.</span><span class="n">SERVICE_NAME</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Mark all services as healthy.</span>
</span></span><span class="line"><span class="cl">    <span class="n">health_pb2_grpc</span><span class="o">.</span><span class="n">add_HealthServicer_to_server</span><span class="p">(</span><span class="n">health_servicer</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">service</span> <span class="ow">in</span> <span class="n">services</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">health_servicer</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="n">health_pb2</span><span class="o">.</span><span class="n">HealthCheckResponse</span><span class="o">.</span><span class="n">SERVING</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">reflection</span><span class="o">.</span><span class="n">enable_server_reflection</span><span class="p">(</span><span class="n">services</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">health_servicer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">sig_handler</span><span class="p">(</span><span class="n">serve_instance</span><span class="p">:</span> <span class="s2">"Serve"</span><span class="p">,</span> <span class="n">sig_num</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"Signal number: </span><span class="si">%s</span><span class="s2"> received, shutting down..."</span><span class="p">,</span> <span class="n">sig_num</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">serve_instance</span><span class="o">.</span><span class="n">health_servicer</span><span class="o">.</span><span class="n">enter_graceful_shutdown</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">serve_instance</span><span class="o">.</span><span class="n">model_instance</span><span class="o">.</span><span class="n">instance_pool</span><span class="o">.</span><span class="n">enter_graceful_shutdown</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="n">serve_instance</span><span class="o">.</span><span class="n">model_instance</span><span class="o">.</span><span class="n">instance_pool</span><span class="o">.</span><span class="n">pool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">serve_instance</span><span class="o">.</span><span class="n">model_instance</span><span class="o">.</span><span class="n">instance_pool</span><span class="o">.</span><span class="n">release</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"Waiting for model instance to be released..."</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">serve_instance</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">stop</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"RPC server shutdown complete"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Server</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="fm">__aenter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">server</span> <span class="o">=</span> <span class="n">grpc</span><span class="o">.</span><span class="n">aio</span><span class="o">.</span><span class="n">server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">futures</span><span class="o">.</span><span class="n">ThreadPoolExecutor</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">max_workers</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">rpc_max_workers</span> <span class="ow">or</span> <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">options</span><span class="o">=</span><span class="n">merge_options</span><span class="p">(</span><span class="n">DEFAULT_SERVER_OPTIONS</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">model_instance</span> <span class="o">=</span> <span class="n">AIServicer</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">clear_work_dirs</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">model_instance</span><span class="o">.</span><span class="n">instance_pool</span><span class="o">.</span><span class="n">datapath</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">ai_pb2_grpc</span><span class="o">.</span><span class="n">add_AIServicer_to_server</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">model_instance</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">server</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">add_insecure_port</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">health_servicer</span> <span class="o">=</span> <span class="n">_configure_maintenance_server</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">server</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">"listening address: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">address</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">sig</span> <span class="ow">in</span> <span class="p">(</span><span class="n">signal</span><span class="o">.</span><span class="n">SIGHUP</span><span class="p">,</span> <span class="n">signal</span><span class="o">.</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">signal</span><span class="o">.</span><span class="n">SIGTERM</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">loop</span><span class="o">.</span><span class="n">add_signal_handler</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">sig</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">s</span><span class="o">=</span><span class="n">sig</span><span class="p">:</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_task</span><span class="p">(</span><span class="n">sig_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">s</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span></span></span></code></pre></div></div>

<h3 class="relative group">zstd 压缩
    <div id="zstd-压缩" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zstd-%e5%8e%8b%e7%bc%a9" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这玩意比 gzip 压缩率大还快，没理由不用，但 gRPC 暂时还没有支持，咋办？魔改呗，把大二机制参数值用 zstd 自己压一遍，收的时候再解就 OK，虽然费了手续，但能省可观的网络 IO，可太值了。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 发</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_prepare_stream_data</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="p">,</span> <span class="n">binary_data</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="nb">bytes</span><span class="p">,</span> <span class="n">Path</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span> <span class="o">*</span><span class="p">,</span> <span class="n">raw_input</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-></span> <span class="n">Generator</span><span class="p">[</span><span class="n">Union</span><span class="p">[</span><span class="n">Raw</span><span class="p">,</span> <span class="n">StreamInput</span><span class="p">],</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="nb">all</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="ow">not</span> <span class="n">k</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"_aipod"</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">kwargs</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span> <span class="s1">'The key argument must not start with "_aipod"'</span>
</span></span><span class="line"><span class="cl">    <span class="n">buffer_size</span> <span class="o">=</span> <span class="n">io</span><span class="o">.</span><span class="n">DEFAULT_BUFFER_SIZE</span> <span class="o">*</span> <span class="mi">4</span>  <span class="c1"># Usually 8k * 4</span>
</span></span><span class="line"><span class="cl">    <span class="n">cctx</span> <span class="o">=</span> <span class="n">zstandard</span><span class="o">.</span><span class="n">ZstdCompressor</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">binary_data</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">binary_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">size</span> <span class="o">></span> <span class="n">buffer_size</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="sa">f</span><span class="s2">"Binary data size </span><span class="si">{</span><span class="n">size</span><span class="si">}</span><span class="s2"> is larger than buffer size </span><span class="si">{</span><span class="n">buffer_size</span><span class="si">}</span><span class="s2">, please pass the original path or iterable object instead of binary data"</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">bytes_io</span> <span class="o">=</span> <span class="n">io</span><span class="o">.</span><span class="n">BytesIO</span><span class="p">(</span><span class="n">binary_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">bytes_io</span><span class="o">.</span><span class="n">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">bin_iter</span> <span class="o">=</span> <span class="n">cctx</span><span class="o">.</span><span class="n">read_to_iter</span><span class="p">(</span><span class="n">bytes_io</span><span class="p">,</span> <span class="n">write_size</span><span class="o">=</span><span class="n">buffer_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">binary_data</span><span class="p">,</span> <span class="p">(</span><span class="nb">str</span><span class="p">,</span> <span class="n">Path</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">        <span class="n">path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">binary_data</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">binary_data</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="k">else</span> <span class="n">binary_data</span>
</span></span><span class="line"><span class="cl">        <span class="n">size</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span>
</span></span><span class="line"><span class="cl">        <span class="n">bin_iter</span> <span class="o">=</span> <span class="n">read_in_zstd_chunks</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">cctx</span><span class="o">=</span><span class="n">cctx</span><span class="p">,</span> <span class="n">chunk_size</span><span class="o">=</span><span class="n">buffer_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s1">'"binary_data" must be bytes, str or Path, got </span><span class="si">{</span><span class="nb">type</span><span class="p">(</span><span class="n">binary_data</span><span class="p">)</span><span class="si">}</span><span class="s1">'</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">kwargs_json</span> <span class="o">=</span> <span class="n">encode_data</span><span class="p">({</span><span class="o">**</span><span class="n">kwargs</span><span class="p">,</span> <span class="n">BIN_SIZE_KEY</span><span class="p">:</span> <span class="n">size</span><span class="p">},</span> <span class="n">cctx</span><span class="o">=</span><span class="n">cctx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">raw_input</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">kwargs_json</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">bin_iter</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">yield</span> <span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># send params first</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">StreamInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">kwargs_json</span><span class="o">=</span><span class="n">kwargs_json</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">version</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">version</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">raw</span><span class="o">=</span><span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="sa">b</span><span class="s2">""</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># then send binary data in chunks</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">bin_iter</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">yield</span> <span class="n">StreamInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">kwargs_json</span><span class="o">=</span><span class="sa">b</span><span class="s2">"</span><span class="si">{}</span><span class="s2">"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">version</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">raw</span><span class="o">=</span><span class="n">Raw</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">chunk</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">kw_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">kwargs_json</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">kw_len</span> <span class="o">></span> <span class="n">buffer_size</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">"Kwargs size </span><span class="si">{</span><span class="n">kw_len</span><span class="si">}</span><span class="s2"> is larger than buffer size </span><span class="si">{</span><span class="n">buffer_size</span><span class="si">}</span><span class="s2">, please use binary_data instead of kwargs to send large data"</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 收</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">_receive_stream</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="n">request_iter</span><span class="p">,</span> <span class="n">var</span><span class="p">:</span> <span class="n">Raw</span><span class="p">,</span> <span class="n">work_dir</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">meta</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">invocation_metadata</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Got stream request: </span><span class="si">{</span><span class="n">meta</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">kwargs</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="n">dctx</span> <span class="o">=</span> <span class="n">zstandard</span><span class="o">.</span><span class="n">ZstdDecompressor</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">lock</span> <span class="o">=</span> <span class="n">FileLock</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">work_dir</span><span class="p">,</span> <span class="s2">".lock"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">compressed_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">work_dir</span><span class="p">,</span> <span class="s2">"compressed"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">decompressed_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">work_dir</span><span class="p">,</span> <span class="s2">"decompressed"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">lock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">compressed_path</span><span class="p">,</span> <span class="s2">"wb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">file_obj</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">async</span> <span class="k">for</span> <span class="n">raw</span> <span class="ow">in</span> <span class="n">request_iter</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">kwargs</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">file_obj</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">raw</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">                <span class="n">kwargs</span> <span class="o">=</span> <span class="n">decode_data</span><span class="p">(</span><span class="n">raw</span><span class="o">.</span><span class="n">data</span><span class="p">,</span> <span class="n">dctx</span><span class="o">=</span><span class="n">dctx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="n">model</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">instance_pool</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"version"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">meta</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"x-func-name"</span><span class="p">,</span> <span class="s2">"chaos"</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">                    <span class="n">context</span><span class="o">.</span><span class="n">set_code</span><span class="p">(</span><span class="n">grpc</span><span class="o">.</span><span class="n">StatusCode</span><span class="o">.</span><span class="n">UNIMPLEMENTED</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">context</span><span class="o">.</span><span class="n">set_details</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                        <span class="sa">f</span><span class="s2">"</span><span class="se">\"</span><span class="si">{</span><span class="n">model</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2">.</span><span class="si">{</span><span class="n">meta</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'x-func-name'</span><span class="p">,</span> <span class="s1">'chaos'</span><span class="p">)</span><span class="si">}</span><span class="se">\"</span><span class="s2"> not implemented yet"</span>
</span></span><span class="line"><span class="cl">                    <span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">var</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">PONG</span><span class="o">.</span><span class="n">encode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">compressed_path</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">file_in</span><span class="p">,</span> <span class="nb">open</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">decompressed_path</span><span class="p">,</span> <span class="s2">"wb"</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span> <span class="k">as</span> <span class="n">file_out</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">dctx</span><span class="o">.</span><span class="n">read_to_iter</span><span class="p">(</span><span class="n">file_in</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="n">file_out</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 这里是我上面说的万能方法，客户端可以通过meta data选择服务端的func</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">meta</span><span class="p">[</span><span class="s2">"x-func-name"</span><span class="p">])(</span>
</span></span><span class="line"><span class="cl">            <span class="n">context</span><span class="p">,</span> <span class="n">decompressed_path</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">var</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">encode_data</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">result</span><span class="o">.</span><span class="n">decode</span><span class="p">()</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">)</span> <span class="k">else</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span></span></span></code></pre></div></div>

<h3 class="relative group">多实例 Nginx 均衡
    <div id="多实例-nginx-均衡" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a4%9a%e5%ae%9e%e4%be%8b-nginx-%e5%9d%87%e8%a1%a1" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>鉴于本框架带载能力太强，实测单次超4GB都稳如老狗，所以渐渐有他组产品也来套用，这时候问题来了：protocol 是固定的，假如我接了 xxx yyy zzz 三个模型，这三个模型不巧又同时被一个 Nginx 代理，他们的 location 是一样的<code>/ai.AI</code> 那咋分流呢？改 protocol 是不可能改的，我懒，走了另一条道，其实包括万能方法<code>chaos</code>以内，都已经体现在上面代码里了。剩下 Nginx 的配置简单提一下：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># x-func-name 可以指定 server 端用哪个方法来处理数据</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 至于数据处理以及不同服务分流、负载均衡等均可以利用 gRPC 的 metadata 功能由客户端自由控制（在服务端实现了相应功能`hello`的前提下）</span>
</span></span><span class="line"><span class="cl"><span class="c1"># result = asyncio.run(model.chaos(bin_path, metadata=[('x-upstream', 'xxx'), ('x-func-name', 'hello')], **kwargs))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">upstream xxx <span class="o">{</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.9:1082<span class="p">;</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.9:1083<span class="p">;</span>
</span></span><span class="line"><span class="cl">    keepalive 2000<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">upstream yyy <span class="o">{</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.10:1082<span class="p">;</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.10:1083<span class="p">;</span>
</span></span><span class="line"><span class="cl">    keepalive 2000<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">upstream zzz <span class="o">{</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.11:1082<span class="p">;</span>
</span></span><span class="line"><span class="cl">    server 192.168.90.11:1083<span class="p">;</span>
</span></span><span class="line"><span class="cl">    keepalive 2000<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">server <span class="o">{</span>
</span></span><span class="line"><span class="cl">    location /ai.AI/chaos <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 客户端可以通过 metadata.x-upstream 参数来指定具体的后端服务</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 不指定默认为 chaos</span>
</span></span><span class="line"><span class="cl">        grpc_pass <span class="nv">$http_x_upstream</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        grpc_read_timeout 600s<span class="p">;</span>
</span></span><span class="line"><span class="cl">        grpc_send_timeout 600s<span class="p">;</span>
</span></span><span class="line"><span class="cl">        grpc_socket_keepalive on<span class="p">;</span>
</span></span><span class="line"><span class="cl">        client_max_body_size 0<span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>

<h2 class="relative group">gRPC Health Checking
    <div id="grpc-health-checking" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#grpc-health-checking" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>参考：https://github.com/grpc/grpc/tree/master/examples/python/xds 实现了 Health Checking 支持，可以通过命令行工具<code>grpcurl</code>进行健康检查</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">> grpcurl --plaintext localhost:50051 grpc.health.v1.Health.Check
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">"status"</span>: <span class="s2">"SERVING"</span>  // <span class="s2">"NOT_SERVING"</span>即表示该实例处于graceful shutdown状态, 不能再接受新的请求
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>
<p>使用 Nginx 时，可以参考：https://www.nginx.com/blog/nginx-plus-r23-released/#New-Features-in-Detail 为服务添加健康检查</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-01-24T01:23:27+08:00
</span></span><span class="line"><span class="cl">update@2023-12-25T03:26:04+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/71</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>AX3600 终极改造：ShellClash 替换 Clash-Meta 核心，完美支持 Hysteria 2</title>
      <link>https://blog.ferstar.org/posts/ax3600-clash-meta-upgrade/</link>
      <pubDate>Tue, 24 Jan 2023 01:17:09 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ax3600-clash-meta-upgrade/</guid>
      <description>针对小米 AX3600 存储不足难题，详解利用 UPX 压缩技术替换 Clash-Meta 核心，实现对 Hysteria 2、TUIC 等新协议的完美支持，让老路由重焕青春。</description><content:encoded><![CDATA[<p>这个路由的 root 包括安装 shellclash 这个帖子介绍的很清楚：https://qust.me/post/ax3600_shellclash/</p>
<p>我自然懒得赘述，下面主要提一下如何替换 clash-meta 核心以支持 hysteria 以及 tuic 这种非主流代理协议的方法 #66</p>
<ol>
<li>当然是先下载 clash 的啦（选 arm64 ）：https://github.com/MetaCubeX/Clash.Meta/releases，我用 Alpha，就是这么头铁</li>
<li>解压&塞到路由器里</li>
</ol>
<p>悲剧的事情发生了：clash-meta 这玩意解压完近 20MB，你看了下折腾完 shellclash 的路由根分区：只剩不到 8MB，灵机一动，掏出加壳压缩大法 upx，同样，先从官网下载 <a href="https://github.com/upx/upx"  target="_blank" rel="noreferrer">https://github.com/upx/upx</a></p>
<p>压缩之：<code>upx -9 clash</code>，最后 7MB 不到的样子，可以轻松塞进路由，替换掉原有 clash</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">root@XiaoQiang:~# du -sh /data/clash/clash
</span></span><span class="line"><span class="cl">6.6M    /data/clash/clash
</span></span><span class="line"><span class="cl">root@XiaoQiang:~# file /data/clash/clash
</span></span><span class="line"><span class="cl">/data/clash/clash: ELF 64-bit LSB executable, ARM aarch64, version <span class="m">1</span> <span class="o">(</span>SYSV<span class="o">)</span>, statically linked, corrupted section header size
</span></span><span class="line"><span class="cl">root@XiaoQiang:~# /data/clash/clash -v
</span></span><span class="line"><span class="cl">Clash Meta alpha-096bb8d linux arm64 with go1.19.5 Mon Jan <span class="m">23</span> 06:08:40 UTC <span class="m">2023</span></span></span></code></pre></div></div>
<p>你以为这就完了？naive，默认固件有个地方乱拉屎 <code>/data/usr/log</code>，莫名其妙就会被 log 文件塞满，导致网络不定时卡顿，所以得弄个任务定时清一波</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">root@XiaoQiang:/data/usr/log# crontab -l
</span></span><span class="line"><span class="cl">*/15 * * * * /usr/sbin/ntpsetclock <span class="m">60</span> log >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1">#* * * * * /usr/sbin/startscene_crontab.lua `/bin/date "+%u %H:%M"`</span>
</span></span><span class="line"><span class="cl"><span class="m">0</span> <span class="m">12</span> * * * /usr/sbin/recordscene_crontab.lua
</span></span><span class="line"><span class="cl"><span class="m">45</span> <span class="m">23</span> * * * /usr/sbin/points_sysset_pro.lua >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1">#*/1 * * * * /usr/sbin/wwdog</span>
</span></span><span class="line"><span class="cl"><span class="m">0</span> <span class="m">20</span> * * * /usr/bin/stat_lan
</span></span><span class="line"><span class="cl"><span class="m">0</span> <span class="m">5</span> * * <span class="m">3</span> /etc/init.d/web_filter_record restart >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="m">0</span> <span class="m">3</span> * * * /etc/init.d/sysapihttpd restart >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="m">0</span> 8,19 * * * /usr/sbin/netdig.sh >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">*/2 * * * * sh /usr/sbin/xqwhc_push.cron >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1">#*/1 * * * * /sbin/trafficd_cpucheck_wifisync.sh</span>
</span></span><span class="line"><span class="cl"><span class="m">0</span> <span class="m">3</span> * * * /usr/sbin/rmportscanresult.sh >/dev/null 2><span class="p">&</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="m">30</span> <span class="m">3</span> * * 1~7 /data/clash/start.sh restart >/dev/null 2><span class="p">&</span><span class="m">1</span> <span class="c1">#每周1~7的3点30分重启clash服务</span>
</span></span><span class="line"><span class="cl">*/3 * * * * rm -rf /data/usr/log/*  <span class="c1"># 清垃圾日志</span>
</span></span><span class="line"><span class="cl">*/10 * * * * <span class="nb">test</span> -n <span class="s2">"</span><span class="k">$(</span>pidof clash<span class="k">)</span><span class="s2">"</span> <span class="o">&&</span> /data/clash/start.sh web_save <span class="c1">#每10分钟保存节点配置</span></span></span></code></pre></div></div>
<p>看着上面这一坨定时任务不禁有想把这玩意刷个原生 openwrt 的冲动，但回想 N 年前折腾 openwrt 孱弱的无线驱动浪费的青春，算了放弃，又不是不能用，要啥自行车。</p>
<p>One more thing，hysteria 这个协议比较吃 CPU，建议适当限制下行参数以降低路由负载，我一般用 100 mbps 足够用了。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-01-24T01:17:09+08:00
</span></span><span class="line"><span class="cl">update@2023-01-24T17:19:55+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/70</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Ubuntu安装第三方内核后如何使用linux-common-tools</title>
      <link>https://blog.ferstar.org/posts/ubuntu-linux-common-tools-install/</link>
      <pubDate>Tue, 24 Jan 2023 01:11:05 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ubuntu-linux-common-tools-install/</guid>
      <description>Ubuntu 有点讨厌，内核相关的包给拆好几个，如果一直官方内核倒也没啥，关键官方内核太老，我一般第一时间就会替换成第三方的内核，比如：https://github.com/xanmod/linux&#xA;</description><content:encoded><![CDATA[<p>Ubuntu 有点讨厌，内核相关的包给拆好几个，如果一直官方内核倒也没啥，关键官方内核太老，我一般第一时间就会替换成第三方的内核，比如：https://github.com/xanmod/linux</p>
<p>但是一旦换成第三方内核，你要使用类似 perf cpupower 这种内核相关命令的时候就会提示下面的错误：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">perf
</span></span><span class="line"><span class="cl">WARNING: perf not found <span class="k">for</span> kernel 6.1.7-x64v3
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  You may need to install the following packages <span class="k">for</span> this specific kernel:
</span></span><span class="line"><span class="cl">    linux-tools-6.1.7-x64v3-xanmod1
</span></span><span class="line"><span class="cl">    linux-cloud-tools-6.1.7-x64v3-xanmod1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  You may also want to install one of the following packages to keep up to date:
</span></span><span class="line"><span class="cl">    linux-tools-xanmod1
</span></span><span class="line"><span class="cl">    linux-cloud-tools-xanmod1</span></span></code></pre></div></div>
<p>缺包，但还贴心的给出了提示，估计是做了一层包装，我们拆开看看：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">~ head -n <span class="m">18</span> <span class="k">$(</span>which perf<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#!/bin/bash</span>
</span></span><span class="line"><span class="cl"><span class="nv">full_version</span><span class="o">=</span><span class="sb">`</span>uname -r<span class="sb">`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># First check for a fully qualified version.</span>
</span></span><span class="line"><span class="cl"><span class="nv">this</span><span class="o">=</span><span class="s2">"/usr/lib/linux-tools/</span><span class="nv">$full_version</span><span class="s2">/`basename </span><span class="nv">$0</span><span class="s2">`"</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -f <span class="s2">"</span><span class="nv">$this</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="nb">exec</span> <span class="s2">"</span><span class="nv">$this</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Removing flavour from version i.e. generic or server.</span>
</span></span><span class="line"><span class="cl"><span class="nv">flavour_abi</span><span class="o">=</span><span class="si">${</span><span class="nv">full_version</span><span class="p">#*-</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">flavour</span><span class="o">=</span><span class="si">${</span><span class="nv">flavour_abi</span><span class="p">#*-</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">version</span><span class="o">=</span><span class="si">${</span><span class="nv">full_version</span><span class="p">%-</span><span class="nv">$flavour</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">this</span><span class="o">=</span><span class="s2">"</span><span class="nv">$0</span><span class="s2">_</span><span class="nv">$version</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -f <span class="s2">"</span><span class="nv">$this</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="nb">exec</span> <span class="s2">"</span><span class="nv">$this</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">...</span></span></code></pre></div></div>
<p>显然是从<code>/usr/lib/linux-tools/$(uname -r)</code>这个目录里去找<code>perl</code>了，解决也很简单：</p>
<ol>
<li>拉内核源码编译perf二进制塞到上面的目录</li>
<li>因为这种工具代码更新频率很低，所以可以直接从低版本官方内核包中拖出来，丢到上面的目录</li>
</ol>
<p>迫于太懒，我肯定选方法二：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir -p /usr/lib/linux-tools/<span class="k">$(</span>uname -r<span class="k">)</span>
</span></span><span class="line"><span class="cl">apt download linux-tools-5.19.0-21
</span></span><span class="line"><span class="cl"><span class="c1"># 把deb解压，/./usr/lib/linux-tools-5.19.0-21/ 目录里的文件全拷贝到 /usr/lib/linux-tools/$(uname -r)</span></span></span></code></pre></div></div>
<p>大概的目录结构是这样：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">➜  ~ uname -a
</span></span><span class="line"><span class="cl">Linux u2004-p13 6.1.7-x64v3-xanmod1 <span class="c1">#0~20230118.a4fbffc SMP PREEMPT_DYNAMIC Wed Jan 18 23:47:06 UTC  x86_64 x86_64 x86_64 GNU/Linux</span>
</span></span><span class="line"><span class="cl">➜  ~ tree /usr/lib/linux-tools/6.1.7-x64v3-xanmod1
</span></span><span class="line"><span class="cl">/usr/lib/linux-tools/6.1.7-x64v3-xanmod1
</span></span><span class="line"><span class="cl">├── acpidbg
</span></span><span class="line"><span class="cl">├── bpftool
</span></span><span class="line"><span class="cl">├── cpupower
</span></span><span class="line"><span class="cl">├── libperf-jvmti.so
</span></span><span class="line"><span class="cl">├── perf
</span></span><span class="line"><span class="cl">├── turbostat
</span></span><span class="line"><span class="cl">├── usbip
</span></span><span class="line"><span class="cl">├── usbipd
</span></span><span class="line"><span class="cl">└── x86_energy_perf_policy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="m">0</span> directories, <span class="m">9</span> files</span></span></code></pre></div></div>
<p>敲个命令试试看：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">➜  ~ cpupower frequency-info
</span></span><span class="line"><span class="cl">analyzing CPU 0:
</span></span><span class="line"><span class="cl">  driver: acpi-cpufreq
</span></span><span class="line"><span class="cl">  CPUs which run at the same hardware frequency: <span class="m">0</span>
</span></span><span class="line"><span class="cl">  CPUs which need to have their frequency coordinated by software: <span class="m">0</span>
</span></span><span class="line"><span class="cl">  maximum transition latency:  Cannot determine or is not supported.
</span></span><span class="line"><span class="cl">  hardware limits: 1.40 GHz - 1.80 GHz
</span></span><span class="line"><span class="cl">  available frequency steps:  1.80 GHz, 1.70 GHz, 1.40 GHz
</span></span><span class="line"><span class="cl">  available cpufreq governors: conservative ondemand userspace powersave performance schedutil
</span></span><span class="line"><span class="cl">  current policy: frequency should be within 1.40 GHz and 1.80 GHz.
</span></span><span class="line"><span class="cl">                  The governor <span class="s2">"performance"</span> may decide which speed to use
</span></span><span class="line"><span class="cl">                  within this range.
</span></span><span class="line"><span class="cl">  current CPU frequency: Unable to call hardware
</span></span><span class="line"><span class="cl">  current CPU frequency: 1.90 GHz <span class="o">(</span>asserted by call to kernel<span class="o">)</span>
</span></span><span class="line"><span class="cl">  boost state support:
</span></span><span class="line"><span class="cl">    Supported: yes
</span></span><span class="line"><span class="cl">    Active: no</span></span></code></pre></div></div>
<p>完美，后续内核有更新的话，只需要重命名一下 /usr/lib/linux-tools/$(uname -r) 即可</p>
<p>有关的几个issue:</p>
<ol>
<li><a href="https://github.com/xanmod/linux/issues/94"  target="_blank" rel="noreferrer">https://github.com/xanmod/linux/issues/94</a></li>
<li><a href="https://github.com/xanmod/linux/issues/121"  target="_blank" rel="noreferrer">https://github.com/xanmod/linux/issues/121</a></li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-01-24T01:11:05+08:00
</span></span><span class="line"><span class="cl">update@2023-01-24T16:51:52+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/69</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>更换Manjaro默认坑爹的grub</title>
      <link>https://blog.ferstar.org/posts/manjaro-replace-default-grub/</link>
      <pubDate>Tue, 24 Jan 2023 00:57:55 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/manjaro-replace-default-grub/</guid>
      <description>前阵子换了个新电脑，重装是不可能的，于是拷一份老机的数据到新机，具体过程不表，基本就是 #32 说的那套。&#xA;搞完发现系统么发启动，具体报错忘了，大概就是找不到启动配置的意思，这种一般就是grub的问题，于是进/boot/efi瞧一瞧：&#xA;</description><content:encoded><![CDATA[<p>前阵子换了个新电脑，重装是不可能的，于是拷一份老机的数据到新机，具体过程不表，基本就是 <a href="https://blog.ferstar.org/post/issue-32/"  target="_blank" rel="noreferrer">#32</a> 说的那套。</p>
<p>搞完发现系统么发启动，具体报错忘了，大概就是找不到启动配置的意思，这种一般就是grub的问题，于是进<code>/boot/efi</code>瞧一瞧：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">cd</span> /boot/efi/EFI/manjaro
</span></span><span class="line"><span class="cl">strings grubx64.efi <span class="p">|</span> grep gpt
</span></span><span class="line"><span class="cl"><span class="c1"># 以下为输出</span>
</span></span><span class="line"><span class="cl">partmap/gpt.c
</span></span><span class="line"><span class="cl">part_gpt
</span></span><span class="line"><span class="cl">grub_gpt_partition_map_iterate
</span></span><span class="line"><span class="cl"><span class="o">(</span>,gpt5<span class="o">)</span>/@/boot/grub</span></span></code></pre></div></div>
<p>好家伙，这里把grub入口写死了，我旧盘root在gpt5，但新盘是gpt2，咋玩呢？一种方法是搞个Manjaro的rescure镜像，从镜像chroot进去给重装一下grub，见：https://wiki.manjaro.org/index.php/UEFI_-_Install_Guide</p>
<p>鉴于对这种写死做法的鄙夷，我决定换一种优雅的解决办法：</p>
<p><del>随便找了个 Ubuntu 的 Server 镜像，从里面把 /efi/{Boot,ubuntu} 一起拷贝出来</del></p>
<p><del>把 /boot/efi/EFI/Boot/bootx64.efi 用 Ubuntu 的替换掉</del></p>
<p>其实完全可以用<code>grub-mkimage</code>这个工具来自定义生成一个个性的efi文件：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">grub-mkimage -o bootx64.efi -O x86_64-efi -p /EFI/helloworld ntfs hfs appleldr <span class="se">\
</span></span></span><span class="line"><span class="cl">  boot cat efi_gop efi_uga elf fat hfsplus iso9660 linux keylayouts memdisk <span class="se">\
</span></span></span><span class="line"><span class="cl">  minicmd part_apple ext2 extcmd xfs xnu part_bsd part_gpt search <span class="se">\
</span></span></span><span class="line"><span class="cl">  search_fs_file chain btrfs loadbios loadenv lvm minix minix2 reiserfs <span class="se">\
</span></span></span><span class="line"><span class="cl">  memrw mmap msdospart scsi loopback normal configfile gzio all_video efi_gop <span class="se">\
</span></span></span><span class="line"><span class="cl">  efi_uga gfxterm gettext <span class="nb">echo</span> boot chain <span class="nb">eval</span> ls <span class="nb">test</span> sleep png gfxmenu part_msdos</span></span></code></pre></div></div>
<p>把 /boot/efi/EFI/ubuntu 复制过去，修改一下 grub.cfg 里面的 uuid 指向新盘 root 分区的 uuid 即可</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">search.fs_uuid f622fc86-161c-42dc-9a19-9c3c756d6ced root
</span></span><span class="line"><span class="cl"><span class="nb">set</span> <span class="nv">prefix</span><span class="o">=(</span><span class="nv">$root</span><span class="o">)</span><span class="s1">'/@/boot/grub'</span>  <span class="c1"># 这里我用的是 btrfs 子卷 @ 作为 rootfs</span>
</span></span><span class="line"><span class="cl">configfile <span class="nv">$prefix</span>/grub.cfg</span></span></code></pre></div></div>
<p>可以看一下 Ubuntu 这个 grubx64.efi 的部分内容 <code>strings grubx64.efi | grep cfg</code></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">%s/grub.cfg
</span></span><span class="line"><span class="cl">feature_net_search_cfg</span></span></code></pre></div></div>
<p>引导路径是这样的：EFI -> Boot -> bootx64.efi -> grubx64.efi -> grub.cfg -> rootfs/grub.cfg -> boot，比 Manjaro 多了按配置查找 grub.cfg 的这一步，我觉得很赞。</p>
<p>当然这个方式有个小瑕疵：如果你用类似 refind 这种 boot manager 的话，会发现启动 Manjaro 的启动图标变成了 Ubuntu，明显这玩意是靠 /boot/efi/EFI/xxx 来判断目标OS的，对于我这种 N 久不关机的人来说，可以忽略不计。</p>
<p>强迫症患者可以在正常进入 Manjaro 以后自行重装 grub，然后删除 /boot/efi/EFI/ubuntu 即可。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># NOTE: I am not responsible for any expired content.
</span></span><span class="line"><span class="cl">create@2023-01-24T00:57:55+08:00
</span></span><span class="line"><span class="cl">update@2023-02-09T15:16:17+08:00
</span></span><span class="line"><span class="cl">comment@https://github.com/ferstar/blog/issues/68</span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
