<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>CYK</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://cyk.pink/</id>
  <link href="https://cyk.pink/" rel="alternate"/>
  <link href="https://cyk.pink/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, CYK</rights>
  <subtitle>记录学习和日常生活</subtitle>
  <title>CYK's Blog</title>
  <updated>2026-10-28T03:10:00.000Z</updated>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="SEO" scheme="https://cyk.pink/tags/SEO/"/>
    <content>
      <![CDATA[<h2 id="百度"><a href="#百度" class="headerlink" title="百度"></a>百度</h2><p>验证方式是往 <code>&lt;head&gt;</code> 里塞一个 meta：</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;baidu-site-verification&quot;</span> <span class="attr">content</span>=<span class="string">&quot;codeva-xxxxxxx&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><p>Butterfly 有现成的位置，<code>_config.butterfly.yml</code> 的 <code>inject.head</code> 里加一行：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;meta</span> <span class="string">name=&quot;baidu-site-verification&quot;</span> <span class="string">content=&quot;codeva-xxxxxxx&quot;</span> <span class="string">/&gt;</span></span><br></pre></td></tr></table></figure><p>验证过了之后提交 sitemap，会看到一个意料之外的东西：配额 0。</p><p>sitemap 提交页 0，API 提交页也是 0。</p><p>新站就是这样。配额按站点信用动态给，要等收录几十篇之后才会涨上去。</p><p>但它限制的只是「你主动推」这一条路。robots.txt 里写好了 <code>Sitemap:</code> 声明，蜘蛛正式抓取之前一定会先读 robots.txt，读到就会来找——这条路不占配额。</p><p>顺带还遇到一次抓取诊断报 <code>DNS无法解析IP</code>，0.001 秒就返回了。同一时间四个公共 DNS 加权威 NS 全是正常的，是它工具自己抽风。隔了半小时再点，过了：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">220.181.108.145  GET /  200  15048</span><br></pre></td></tr></table></figure><p><code>220.181.108.x</code> 是百度蜘蛛的官方段。</p><h2 id="Google"><a href="#Google" class="headerlink" title="Google"></a>Google</h2><p>文件验证。下载回来那个 <code>googleXXXXXXXX.html</code>，丢到 <code>source/</code> 根目录。</p><p>有个地方要处理一下——hexo 有可能把它当普通文件复制，也可能套上主题模板，那就废了。加进 <code>skip_render</code> 保险：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">skip_render:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">googleXXXXXXXX.html</span></span><br></pre></td></tr></table></figure><p>文件内容就一行：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">google-site-verification: googleXXXXXXXX.html</span><br></pre></td></tr></table></figure><p>上线之后 <code>curl</code> 一下，确认返回的是这行纯文本，不是一堆 HTML。</p><h2 id="Bing"><a href="#Bing" class="headerlink" title="Bing"></a>Bing</h2><p>后台点了几次都是 <code>Unexpected error occurred</code>。从 Google Search Console 导入也不行，提示找不到站点。</p><p>meta 验证码它倒是给了，但后台本身挂了，验证码也用不上。</p><h2 id="绕过去：IndexNow"><a href="#绕过去：IndexNow" class="headerlink" title="绕过去：IndexNow"></a>绕过去：IndexNow</h2><p>IndexNow 这套协议不需要在后台验证站点。规则就一条：在站点根目录放一个以 key 命名的文本文件，文件内容就是 key 本身。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">https://cyk.pink/a984ba7b3eb04e828fea37c652a9ef09.txt</span><br><span class="line">内容：a984ba7b3eb04e828fea37c652a9ef09</span><br></pre></td></tr></table></figure><p>然后 POST 一批 URL 过去：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -X POST https://api.indexnow.org/indexnow \</span><br><span class="line">  -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data-binary @urls.json</span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;host&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cyk.pink&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;a984ba7b3eb04e828fea37c652a9ef09&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;keyLocation&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://cyk.pink/a984ba7b3eb04e828fea37c652a9ef09.txt&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;urlList&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;https://cyk.pink/&quot;</span><span class="punctuation">,</span> <span class="string">&quot;...&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>第一次在本机发的，一直 403。同一个 key 换成 GET 方式、把参数拼到 URL 上，返回 200。</p><p>区别只在参数放 body 还是放 URL。本地有个代理会改写 POST 的 body，到服务端的时候已经是空的了。</p><p>改到服务器上直接 curl，200，217 条一次推完。</p><p>一小时后 Bingbot 就来了：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">157.55.39.60  GET /sitemap.xml  200  ×2</span><br></pre></td></tr></table></figure><h2 id="现在三家什么状态"><a href="#现在三家什么状态" class="headerlink" title="现在三家什么状态"></a>现在三家什么状态</h2><table><thead><tr><th>平台</th><th>验证</th><th>抓取</th></tr></thead><tbody><tr><td>百度</td><td>meta 通过</td><td>首次抓取成功</td></tr><tr><td>Google</td><td>文件通过</td><td>47 次</td></tr><tr><td>Bing</td><td>后台仍报错，用 IndexNow 绕过</td><td>已抓 sitemap</td></tr></tbody></table><p>平台后台不是必须的。sitemap 和 robots 做对，蜘蛛自己会来；IndexNow 是另一条不需要账号的路。</p>]]>
    </content>
    <id>https://cyk.pink/posts/e7c5ef61.html</id>
    <link href="https://cyk.pink/posts/e7c5ef61.html"/>
    <published>2026-10-28T03:10:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="百度"><a href="#百度" class="headerlink" title="百度"></a>百度</h2><p>验证方式是往 <code>&lt;head&gt;</code> 里塞一个 meta：</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;baidu-site-verification&quot;</span> <span class="attr">content</span>=<span class="string">&quot;codeva-xxxxxxx&quot;</span>]]>
    </summary>
    <title>三个站长平台跑一遍：验证、sitemap 和 IndexNow</title>
    <updated>2026-10-28T03:10:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="Hexo" scheme="https://cyk.pink/tags/Hexo/"/>
    <category term="Git" scheme="https://cyk.pink/tags/Git/"/>
    <content>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>sitemap 提交给 Google 之后一直显示「无法抓取」。用 Googlebot 的 UA 去取是 200，干脆把 225 条 URL 全跑一遍，揪出 8 条 404：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/404.html</span><br><span class="line">/tags/Docker/   /tags/DOCKER/   /tags/Java/</span><br><span class="line">/tags/kotlin/   /tags/python/   /tags/hexo/   /tags/nginx/</span><br></pre></td></tr></table></figure><p><code>/404.html</code> 是另一回事（nginx 里设成了 internal，本来就不该进 sitemap）。先说那 7 个标签页。</p><h2 id="目录名对不上"><a href="#目录名对不上" class="headerlink" title="目录名对不上"></a>目录名对不上</h2><p>对着服务器挨个查：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sitemap 写 Docker  → 服务器上是 docker</span><br><span class="line">sitemap 写 Java    → 服务器上是 JAVA</span><br><span class="line">sitemap 写 kotlin  → 服务器上是 Kotlin</span><br></pre></td></tr></table></figure><p>每个都差一层大小写，而且新旧一一对应。</p><p>根子在文章的 front-matter。同一个东西，有的写 <code>Docker</code>，有的写 <code>DOCKER</code>，有的写 <code>docker</code>。Hexo 把它们当成三个不同的标签，生成三个目录。</p><h2 id="三个目录在本地是同一个"><a href="#三个目录在本地是同一个" class="headerlink" title="三个目录在本地是同一个"></a>三个目录在本地是同一个</h2><p>因为我在 Windows 上写博客。</p><p>文件名不区分大小写，<code>public/tags/Docker/</code> 和 <code>public/tags/docker/</code> 指向同一块地方。构建时后生成的覆盖先生成的，最后只剩一种拼写，git 也就只记录了这一种。</p><p>扫了 92 篇、247 处标签引用，6 组冲突，改了 15 个文件 15 处，统一成首字母大写。</p><h2 id="重新部署，还是-404"><a href="#重新部署，还是-404" class="headerlink" title="重新部署，还是 404"></a>重新部署，还是 404</h2><p><code>hexo clean</code> 之后重建、<code>hexo deploy</code>，线上照样打不开 <code>/tags/Docker/</code>。</p><p>爬上去看目录，还是 <code>DOCKER</code>。</p><p>不是部署失败。<code>.deploy_git</code> 那个仓库的索引里记着旧路径 <code>tags/DOCKER/index.html</code>，新构建是 <code>tags/Docker/index.html</code>。<strong>Windows 上的 git 也认为这是同一个文件</strong>（<code>core.ignorecase</code> 默认开着），于是只更新内容，路径名一个字都没动。</p><p>改了大小写，但传不到 Linux 服务器上。</p><h2 id="换一个干净的索引推"><a href="#换一个干净的索引推" class="headerlink" title="换一个干净的索引推"></a>换一个干净的索引推</h2><p><code>.deploy_git</code> 是 hexo-deployer-git 自己维护的，没去碰它。直接拿构建产物新建一个仓库推上去：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">D=$(<span class="built_in">mktemp</span> -d)</span><br><span class="line"><span class="built_in">cp</span> -r public/. <span class="string">&quot;<span class="variable">$D</span>&quot;</span>/</span><br><span class="line"><span class="built_in">cd</span> <span class="string">&quot;<span class="variable">$D</span>&quot;</span></span><br><span class="line">git init -q &amp;&amp; git add -A</span><br><span class="line">git commit -q -m <span class="string">&quot;deploy&quot;</span></span><br><span class="line">git push -f ubuntu@server:/var/repo/cykBlog.git HEAD:master</span><br></pre></td></tr></table></figure><p><code>git init</code> 出来是空索引，路径按实际大小写记录，推上去服务器就对了。</p><h2 id="还有一步不能省"><a href="#还有一步不能省" class="headerlink" title="还有一步不能省"></a>还有一步不能省</h2><p><code>hexo clean</code>。</p><p>不清的话 <code>public/</code> 里旧的大小写目录还在，和新目录继续打架，谁覆盖谁看运气。</p><h2 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h2><p>7 个标签页恢复。sitemap 从 225 条降到 217 条——不是少了内容，是 6 组重复标签合并了。线上全量跑一遍，0 死链。</p><p>一个标签拼错三个写法，最后要动到 git 索引才能修好。这种东西不上线是发现不了的。</p>]]>
    </content>
    <id>https://cyk.pink/posts/1f975433.html</id>
    <link href="https://cyk.pink/posts/1f975433.html"/>
    <published>2026-10-28T03:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>sitemap 提交给 Google 之后一直显示「无法抓取」。用 Googlebot 的 UA 去取是 200，干脆把 225 条 URL 全跑一遍，揪出 8 条 404：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/404.html</span><br><span class="line">/tags/Docker/   /tags/DOCKER/   /tags/Java/</span><br><span class="line">/tags/kotlin/   /tags/python/   /tags/hexo/   /tags/nginx/</span><br></pre></td></tr></table></figure>

<p><code>/404.html</code>]]>
    </summary>
    <title>标签大小写引发的连锁反应：从 sitemap 死链到 git 不改名</title>
    <updated>2026-10-28T03:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="Nginx" scheme="https://cyk.pink/tags/Nginx/"/>
    <content>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p><code>www.cyk.pink</code> 打不开，浏览器直接报证书错误。点「继续访问」之后又能看到完整站点。</p><h2 id="先查再改"><a href="#先查再改" class="headerlink" title="先查再改"></a>先查再改</h2><p>我一开始以为是 DNS 没做。查完发现想错了：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">DNS        www.cyk.pink → 43.143.218.149       正常</span><br><span class="line">80 端口     → 301 https://cyk.pink/            正常</span><br><span class="line">443        server_name cyk.pink www.cyk.pink   两个域名同一个块</span><br><span class="line">证书 SAN    DNS:cyk.pink                        没有 www</span><br></pre></td></tr></table></figure><p>两个问题叠在一起：证书不认 www，443 又把 www 和主域塞进同一个 server 块。所以用户在证书警告页点继续之后，看到的是同一个站。</p><p>同一份内容挂在两个域名下，这才是要修的地方。</p><h2 id="扩证书失败"><a href="#扩证书失败" class="headerlink" title="扩证书失败"></a>扩证书失败</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> certbot --nginx -d cyk.pink -d www.cyk.pink --<span class="built_in">expand</span></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Domain: www.cyk.pink</span><br><span class="line">Detail: During secondary validation: Timeout during connect (likely firewall problem)</span><br></pre></td></tr></table></figure><p>报错里写着 firewall，就去翻防火墙，80 和 443 对 <code>0.0.0.0/0</code> 全开。又换四个公共 DNS 查解析，也都正常。</p><p>Let’s Encrypt 现在是多节点校验，主节点过了、某个 secondary 节点超时也会判失败。跟我这边没关系。</p><p>换成 webroot，同时把 ACME 的验证路径从跳转里单独捞出来：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span> cyk.pink www.cyk.pink;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span><span class="regexp"> ^~</span> /.well-known/acme-challenge/ &#123;</span><br><span class="line">        <span class="attribute">root</span> /var/www/cyk.pink;</span><br><span class="line">        <span class="attribute">default_type</span> <span class="string">&quot;text/plain&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="attribute">return</span> <span class="number">301</span> https://cyk.pink<span class="variable">$request_uri</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里踩了个坑。</p><p>80 端口原来是这么一句：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span> cyk.pink www.cyk.pink;</span><br><span class="line">    <span class="attribute">return</span> <span class="number">301</span> https://cyk.pink<span class="variable">$request_uri</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我直接在下面加了 <code>location</code>，以为能把验证路径拦下来。没拦住。</p><p>server 级的 <code>return</code> 在 rewrite 阶段就执行完了，根本轮不到 location 匹配。放行必须把跳转挪进 <code>location /</code> 里。</p><p>改完一次过：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> certbot certonly --webroot -w /var/www/cyk.pink \</span><br><span class="line">  --cert-name cyk.pink -d cyk.pink -d www.cyk.pink --<span class="built_in">expand</span></span><br></pre></td></tr></table></figure><h2 id="443-拆成两块"><a href="#443-拆成两块" class="headerlink" title="443 拆成两块"></a>443 拆成两块</h2><p>主域的 <code>server_name</code> 只留裸域，www 单独一个块，只负责跳转：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">443</span> ssl http2;</span><br><span class="line">    <span class="attribute">server_name</span> www.cyk.pink;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">ssl_certificate</span>     /etc/letsencrypt/live/cyk.pink/fullchain.pem;</span><br><span class="line">    <span class="attribute">ssl_certificate_key</span> /etc/letsencrypt/live/cyk.pink/privkey.pem;</span><br><span class="line">    <span class="attribute">include</span> /etc/letsencrypt/options-ssl-nginx.conf;</span><br><span class="line">    <span class="attribute">ssl_dhparam</span> /etc/letsencrypt/ssl-dhparams.pem;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">return</span> <span class="number">301</span> https://cyk.pink<span class="variable">$request_uri</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">https://www.cyk.pink/                 → 301 → https://cyk.pink/</span><br><span class="line">https://www.cyk.pink/posts/xxx.html   → 301 → https://cyk.pink/posts/xxx.html</span><br><span class="line">SAN: DNS:cyk.pink, DNS:www.cyk.pink</span><br></pre></td></tr></table></figure><p>路径能带过去，不是一律回首页。</p><p>续期也验了一遍：<code>certbot renew --dry-run</code> 通过。签发方式自动改成 webroot 了，<code>webroot_path</code> 记在 renewal 配置里，以后不用再管。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9f34c5bf.html</id>
    <link href="https://cyk.pink/posts/9f34c5bf.html"/>
    <published>2026-10-28T02:50:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p><code>www.cyk.pink</code> 打不开，浏览器直接报证书错误。点「继续访问」之后又能看到完整站点。</p>
<h2 id="先查再改"><a href="#先查再改" class="headerlink" title="先查再改"></a>先查再改</h2><p>我一开始以为是 DNS 没做。查完发现想错了：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">DNS        www.cyk.pink → 43.143.218.149       正常</span><br><span class="line">80 端口     → 301 https://cyk.pink/            正常</span><br><span class="line">443]]>
    </summary>
    <title>www 打不开：一张证书和两个 server 块</title>
    <updated>2026-10-28T02:50:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="Hexo" scheme="https://cyk.pink/tags/Hexo/"/>
    <category term="SEO" scheme="https://cyk.pink/tags/SEO/"/>
    <content>
      <![CDATA[<h2 id="起点"><a href="#起点" class="headerlink" title="起点"></a>起点</h2><p>92 篇文章，<code>site:cyk.pink</code> 一条结果都没有。</p><p>不是排名差，是搜索引擎不知道有这么一个站。查了一下，sitemap、robots、feed 三个地址全是 404，站长平台一个都没提交过。三年里我做完 <code>hexo deploy</code> 就关浏览器。</p><h2 id="先看已经有什么"><a href="#先看已经有什么" class="headerlink" title="先看已经有什么"></a>先看已经有什么</h2><p>免得重复劳动：</p><table><thead><tr><th>项目</th><th>结果</th></tr></thead><tbody><tr><td>http → https</td><td>301</td></tr><tr><td>404 页面</td><td>真 404</td></tr><tr><td>canonical &#x2F; og &#x2F; article meta</td><td>有</td></tr><tr><td>gzip</td><td>开</td></tr><tr><td>sitemap &#x2F; robots &#x2F; feed</td><td>全 404</td></tr><tr><td>ld+json</td><td>无</td></tr><tr><td>站点描述</td><td>「我是CYK」</td></tr></tbody></table><p>前三行是之前折腾 nginx 时顺手做好的，不用再碰。</p><h2 id="sitemap-和-feed"><a href="#sitemap-和-feed" class="headerlink" title="sitemap 和 feed"></a>sitemap 和 feed</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm i hexo-generator-sitemap hexo-generator-feed</span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">sitemap:</span></span><br><span class="line">  <span class="attr">path:</span> <span class="string">sitemap.xml</span></span><br><span class="line">  <span class="attr">tags:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">categories:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">feed:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">atom</span></span><br><span class="line">  <span class="attr">path:</span> <span class="string">atom.xml</span></span><br><span class="line">  <span class="attr">limit:</span> <span class="number">20</span></span><br></pre></td></tr></table></figure><p>百度专用的 <code>hexo-generator-baidu-sitemap</code> 没装。那个包停在 Hexo 4 时代，装上直接跑不起来，而百度现在也读标准 sitemap。</p><p>生成完 225 条 URL。</p><h2 id="robots-txt"><a href="#robots-txt" class="headerlink" title="robots.txt"></a>robots.txt</h2><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">User-agent: *</span><br><span class="line">Allow: /</span><br><span class="line"></span><br><span class="line">Sitemap: https://cyk.pink/sitemap.xml</span><br><span class="line">Sitemap: https://cyk.pink/atom.xml</span><br></pre></td></tr></table></figure><p>放在 <code>source/</code> 根目录，构建时原样复制到站点根。</p><h2 id="主题不输出-keywords"><a href="#主题不输出-keywords" class="headerlink" title="主题不输出 keywords"></a>主题不输出 keywords</h2><p>在 <code>_config.yml</code> 里配了 <code>keywords</code>，重新构建，页面里搜不到 <code>&lt;meta name=&quot;keywords&quot;&gt;</code>。翻主题 layout 才确认，Butterfly 压根没写这一行，配了等于没配。</p><p>Google 不看 keywords，百度和其他爬虫还读。既然要补，就从已经渲染好的 HTML 里把文章标签抠出来拼：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">filter</span>.<span class="title function_">register</span>(<span class="string">&#x27;after_render:html&#x27;</span>, <span class="keyword">function</span> (<span class="params">str</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="regexp">/&lt;meta name=&quot;keywords&quot;/</span>.<span class="title function_">test</span>(str)) <span class="keyword">return</span> str</span><br><span class="line">  <span class="keyword">const</span> tags = []</span><br><span class="line">  <span class="keyword">const</span> re = <span class="regexp">/&lt;meta property=&quot;article:tag&quot; content=&quot;([^&quot;]*)&quot;/g</span></span><br><span class="line">  <span class="keyword">let</span> m</span><br><span class="line">  <span class="keyword">while</span> ((m = re.<span class="title function_">exec</span>(str)) !== <span class="literal">null</span>) tags.<span class="title function_">push</span>(m[<span class="number">1</span>])</span><br><span class="line">  <span class="keyword">const</span> kw = tags.<span class="property">length</span> ? tags.<span class="title function_">join</span>(<span class="string">&#x27;,&#x27;</span>) : hexo.<span class="property">config</span>.<span class="property">keywords</span></span><br><span class="line">  <span class="keyword">return</span> kw ? str.<span class="title function_">replace</span>(<span class="string">&#x27;&lt;/head&gt;&#x27;</span>, <span class="string">`&lt;meta name=&quot;keywords&quot; content=&quot;<span class="subst">$&#123;kw&#125;</span>&quot;&gt;\n&lt;/head&gt;`</span>) : str</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>放 <code>scripts/</code> 目录，hexo 启动时会自动加载。</p><h2 id="结构化数据"><a href="#结构化数据" class="headerlink" title="结构化数据"></a>结构化数据</h2><p>主题里也没有 <code>ld+json</code>。没有它 Google 不给富摘要。</p><p>同一个 filter 接着写，一次输出四个类型：全站 <code>WebSite</code> + <code>Person</code>，文章页再加 <code>BlogPosting</code> 和 <code>BreadcrumbList</code>，用 <code>@graph</code> 串起来。</p><p>数据不用问 hexo 内部对象，从渲染好的 HTML 里反解更省事：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> url = <span class="title function_">pick</span>(html, <span class="regexp">/&lt;link rel=&quot;canonical&quot; href=&quot;([^&quot;]+)&quot;/</span>)</span><br><span class="line"><span class="keyword">const</span> isArticle = <span class="regexp">/&lt;meta property=&quot;og:type&quot; content=&quot;article&quot;/</span>.<span class="title function_">test</span>(html)</span><br></pre></td></tr></table></figure><p>两个地方要处理。</p><p><code>JSON.stringify</code> 之后把 <code>&lt;</code> 换成 <code>\u003c</code>。不换的话，文章正文里只要出现一次 <code>&lt;/script&gt;</code>，整段脚本就提前闭合，后面全部错位。</p><p>整个 filter 包在 try&#x2F;catch 里。SEO 脚本出问题最多是没加标签，不该让整个站构建失败。</p><p>285 个页面，284 个带上了，缺口是 404 页——它没有 canonical，被跳过了。</p><h2 id="顺手清一条死链"><a href="#顺手清一条死链" class="headerlink" title="顺手清一条死链"></a>顺手清一条死链</h2><p>主题自带一个 404 generator，会生成 <code>/404.html</code>，sitemap 插件把它当普通页面收了进去。但 nginx 里写着 <code>location = /404.html &#123; internal; &#125;</code>，外部访问必然 404。</p><p>一条不多，但没必要留着。</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">filter</span>.<span class="title function_">register</span>(<span class="string">&#x27;after_generate&#x27;</span>, clean)</span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">filter</span>.<span class="title function_">register</span>(<span class="string">&#x27;after_exit&#x27;</span>, clean)</span><br></pre></td></tr></table></figure><p>两个时机都挂，是因为它会和 sitemap 插件抢写入顺序。只挂 <code>after_generate</code> 的时候，加完文章重新生成，那个 <code>/404.html</code> 又被写回去了一次。</p><h2 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h2><p>sitemap 217 条，全量校验 0 死链，284&#x2F;285 页有结构化数据。</p><p>真正的难点不在这几个配置，在于要先把「我以为的」和「实际缺的」分开——sitemap 和 ld+json 是真缺，其他都是瞎操心。</p>]]>
    </content>
    <id>https://cyk.pink/posts/6ec067b8.html</id>
    <link href="https://cyk.pink/posts/6ec067b8.html"/>
    <published>2026-10-28T02:40:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="起点"><a href="#起点" class="headerlink" title="起点"></a>起点</h2><p>92 篇文章，<code>site:cyk.pink</code> 一条结果都没有。</p>
<p>不是排名差，是搜索引擎不知道有这么一个站。查了一下，sitemap、robots、feed 三个地址全是 404，站长平台一个都没提交过。三年里我做完 <code>hexo deploy</code> 就关浏览器。</p>
<h2 id="先看已经有什么"><a href="#先看已经有什么" class="headerlink" title="先看已经有什么"></a>先看已经有什么</h2><p>免得重复劳动：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>结果</th>
</tr>
</thead>
<tbody><tr>
<td>http → https</td>
<td>301</td>
</tr>
<tr>
<td>404 页面</td>
<td>真]]>
    </summary>
    <title>sitemap、robots 和结构化数据：静态博客的收录地基</title>
    <updated>2026-10-28T02:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Hexo" scheme="https://cyk.pink/tags/Hexo/"/>
    <category term="Butterfly" scheme="https://cyk.pink/tags/Butterfly/"/>
    <content>
      <![CDATA[<h2 id="原来的样子"><a href="#原来的样子" class="headerlink" title="原来的样子"></a>原来的样子</h2><p>左上角三个圆点，右边一个大写加粗的语言名，还带一圈投影。做演示截图够用，放在正经文章里太吵。</p><p><code>highlight_theme: mac</code> 关掉的只是配色，那三个圆点是写死在 CSS 里的。</p><h2 id="圆点不是三个-div"><a href="#圆点不是三个-div" class="headerlink" title="圆点不是三个 div"></a>圆点不是三个 div</h2><p>是一个伪元素，用 box-shadow 投出来的：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#article-container</span> <span class="selector-tag">figure</span><span class="selector-class">.highlight</span> <span class="selector-class">.highlight-tools</span><span class="selector-pseudo">:after</span> &#123;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  <span class="attribute">left</span>: <span class="number">14px</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">12px</span>;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">12px</span>;</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">50%</span>;</span><br><span class="line">  <span class="attribute">background</span>: <span class="number">#fc625d</span>;</span><br><span class="line">  <span class="attribute">box-shadow</span>: <span class="number">20px</span> <span class="number">0</span> <span class="number">#fdbc40</span>, <span class="number">40px</span> <span class="number">0</span> <span class="number">#35cd4b</span>;</span><br><span class="line">  <span class="attribute">content</span>: <span class="string">&#x27; &#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一个红点靠 box-shadow 投出黄绿两个。所以关掉就是让它没有内容：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#article-container</span> <span class="selector-tag">figure</span><span class="selector-class">.highlight</span> <span class="selector-class">.highlight-tools</span><span class="selector-pseudo">:after</span> &#123;</span><br><span class="line">  <span class="attribute">content</span>: none;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="语言标签的位置是写死的"><a href="#语言标签的位置是写死的" class="headerlink" title="语言标签的位置是写死的"></a>语言标签的位置是写死的</h2><p><code>.code-lang</code> 是绝对定位，mac 模式下 <code>left: 75px</code>——正好给那三个圆点让位。圆点删了之后它还杵在 75px，得一起改。</p><p>顺手从 absolute 改成 static。工具条本来就是 flex，改完自然靠左，不用再算偏移：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#article-container</span> <span class="selector-tag">figure</span><span class="selector-class">.highlight</span> <span class="selector-class">.highlight-tools</span> <span class="selector-class">.code-lang</span> &#123;</span><br><span class="line">  <span class="attribute">position</span>: static;</span><br><span class="line">  <span class="attribute">left</span>: auto;</span><br><span class="line">  <span class="attribute">padding-left</span>: <span class="number">9px</span>;</span><br><span class="line">  <span class="attribute">border-left</span>: <span class="number">2px</span> solid <span class="number">#49b1f5</span>;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">12px</span>;</span><br><span class="line">  <span class="attribute">font-weight</span>: <span class="number">400</span>;</span><br><span class="line">  <span class="attribute">letter-spacing</span>: .<span class="number">04em</span>;</span><br><span class="line">  <span class="attribute">text-transform</span>: none;</span><br><span class="line">  <span class="attribute">color</span>: <span class="built_in">rgba</span>(<span class="number">255</span>,<span class="number">255</span>,<span class="number">255</span>,.<span class="number">42</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>大写改回小写，去掉加粗，前面补一条短竖线。</p><h2 id="卡片和工具条"><a href="#卡片和工具条" class="headerlink" title="卡片和工具条"></a>卡片和工具条</h2><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#article-container</span> <span class="selector-tag">figure</span><span class="selector-class">.highlight</span> &#123;</span><br><span class="line">  <span class="attribute">margin</span>: <span class="number">0</span> <span class="number">0</span> <span class="number">26px</span>;</span><br><span class="line">  <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,.<span class="number">06</span>);</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">10px</span>;</span><br><span class="line">  <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">1px</span> <span class="number">2px</span> <span class="built_in">rgba</span>(<span class="number">16</span>,<span class="number">24</span>,<span class="number">40</span>,.<span class="number">04</span>), <span class="number">0</span> <span class="number">10px</span> <span class="number">24px</span> -<span class="number">14px</span> <span class="built_in">rgba</span>(<span class="number">16</span>,<span class="number">24</span>,<span class="number">40</span>,.<span class="number">15</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-id">#article-container</span> <span class="selector-tag">figure</span><span class="selector-class">.highlight</span> <span class="selector-class">.highlight-tools</span> &#123;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">34px</span>;</span><br><span class="line">  <span class="attribute">min-height</span>: <span class="number">34px</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span> <span class="number">14px</span>;</span><br><span class="line">  <span class="attribute">background</span>: transparent;</span><br><span class="line">  <span class="attribute">border-bottom</span>: <span class="number">1px</span> solid <span class="built_in">rgba</span>(<span class="number">255</span>,<span class="number">255</span>,<span class="number">255</span>,.<span class="number">07</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工具条背景透明，只留一条 1px 的分隔线。</p><h2 id="底部那个-8px"><a href="#底部那个-8px" class="headerlink" title="底部那个 8px"></a>底部那个 8px</h2><p><code>pre</code> 的 padding 我一开始上下都给 15px，结果每个块底部都空出一大截，怎么调都对不齐。</p><p>原因是 hexo 输出的每一行后面都跟一个 <code>&lt;br&gt;</code>，最后一行也不例外，等于凭空多了个空行：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&lt;span class=&quot;line&quot;&gt;docker search svn&lt;/span&gt;&lt;br&gt;</span><br><span class="line">&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;</span><br><span class="line">&lt;span class=&quot;line&quot;&gt;docker pull xxx&lt;/span&gt;&lt;br&gt;   ← 末尾这个</span><br></pre></td></tr></table></figure><p>所以底部写 8px 就够了，多出来的空间是那个 <code>&lt;br&gt;</code> 撑的。</p><h2 id="颜色不要跟主题走"><a href="#颜色不要跟主题走" class="headerlink" title="颜色不要跟主题走"></a>颜色不要跟主题走</h2><p>主题的代码块底色在浅色和深色页面下<strong>都是深色</strong>（<code>#212121</code> &#x2F; <code>#171717</code>）。所以标签和按钮的文字颜色不能跟着页面主题走，否则浅色模式下变成深灰压深灰，基本看不见。</p><p>这两处我直接写死成浅色，只有卡片的边框和投影用了 <code>[data-theme=&quot;dark&quot;]</code> 做区分。</p><h2 id="放在哪"><a href="#放在哪" class="headerlink" title="放在哪"></a>放在哪</h2><p>全部追加在 <code>source/css/cyk.css</code> 末尾，通过 <code>_config.butterfly.yml</code> 的 <code>inject.head</code> 引进来：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;link</span> <span class="string">rel=&quot;stylesheet&quot;</span> <span class="string">href=&quot;/css/cyk.css&quot;</span> <span class="string">media=&quot;defer&quot;</span> <span class="string">onload=&quot;this.media=&#x27;all&#x27;&quot;&gt;</span></span><br></pre></td></tr></table></figure><p>主题目录一个字没动，以后升级主题不会冲突。</p>]]>
    </content>
    <id>https://cyk.pink/posts/b303c1d5.html</id>
    <link href="https://cyk.pink/posts/b303c1d5.html"/>
    <published>2026-10-28T02:30:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="原来的样子"><a href="#原来的样子" class="headerlink" title="原来的样子"></a>原来的样子</h2><p>左上角三个圆点，右边一个大写加粗的语言名，还带一圈投影。做演示截图够用，放在正经文章里太吵。</p>
<p><code>highlight_theme: mac</code> 关掉的只是配色，那三个圆点是写死在 CSS 里的。</p>
<h2 id="圆点不是三个-div"><a href="#圆点不是三个-div" class="headerlink" title="圆点不是三个 div"></a>圆点不是三个 div</h2><p>是一个伪元素，用 box-shadow 投出来的：</p>
<figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-id">#article-container</span> <span]]>
    </summary>
    <title>把 Butterfly 的代码块改成极简样式</title>
    <updated>2026-10-28T02:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="Docker" scheme="https://cyk.pink/tags/Docker/"/>
    <category term="Twikoo" scheme="https://cyk.pink/tags/Twikoo/"/>
    <category term="Nginx" scheme="https://cyk.pink/tags/Nginx/"/>
    <category term="DNS" scheme="https://cyk.pink/tags/DNS/"/>
    <content>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>博客的评论区一直在转圈，一直以为是「没人评论所以空的」。</p><p>直到有一天为了查别的问题去调评论接口，发现请求根本没回来：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">curl -X POST https://xxx.vercel.app/ → 超时</span><br></pre></td></tr></table></figure><h2 id="第一步：先看-DNS，别急着改代码"><a href="#第一步：先看-DNS，别急着改代码" class="headerlink" title="第一步：先看 DNS，别急着改代码"></a>第一步：先看 DNS，别急着改代码</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nslookup xxx.vercel.app</span><br></pre></td></tr></table></figure><p>结果：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Addresses:  2a03:2880:f12c:183:face:b00c:0:25de</span><br><span class="line">            31.13.85.2</span><br></pre></td></tr></table></figure><p><code>face:b00c</code> 是 Facebook 的 IPv6 特征段，<code>31.13.x</code> 也是 Facebook 的地址块。再查一个对照组：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nslookup vercel.com        → 64.239.123.1   ✅ 正常</span><br><span class="line">nslookup test.vercel.app   → 208.43.170.231 + face:b00c  ❌ 同样被劫持</span><br></pre></td></tr></table></figure><p>结论：**<code>*.vercel.app</code> 整段被 DNS 污染**，解析结果全被指到 Facebook 的 IP 上，所以永远连不上。Vercel 本身没问题，<code>vercel.com</code> 是好的。</p><p>这也解释了为什么有些人抱怨「Vercel 上的服务国内打不开」——不是平台的问题，是域名被挡住了。</p><h2 id="第二步：自定义域名能用，但有版本坑"><a href="#第二步：自定义域名能用，但有版本坑" class="headerlink" title="第二步：自定义域名能用，但有版本坑"></a>第二步：自定义域名能用，但有版本坑</h2><p>我早先给评论服务绑过一个自定义域（<code>twikoo.xxx.com</code>，CNAME 到 Vercel 的 DNS），这个域名没被污染，200 正常响应。</p><p>但调接口返回：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;code&quot;</span><span class="punctuation">:</span><span class="number">1001</span><span class="punctuation">,</span><span class="attr">&quot;message&quot;</span><span class="punctuation">:</span><span class="string">&quot;请更新 Twikoo 云函数至最新版本&quot;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>按字面意思，应该去升级后端。于是我把后端镜像从头到尾换了几个版本——<strong>升到最新还是 1001</strong>。</p><p>回头看源码才发现，这句文案是误导的：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// @twikoojs/common</span></span><br><span class="line"><span class="attr">EVENT_NOT_EXIST</span>: <span class="number">1001</span>,</span><br></pre></td></tr></table></figure><p><strong>1001 的真实含义是「事件不存在」。</strong></p><p>这就对上了：前端加载的是 1.6.x，发的是 <code>GET_COMMENTS</code> 事件；后端早就是 2.x，事件名变了，直接不认。而提示文案还留在旧版本那句话上。</p><p>验证方法：换成不涉及事件名的接口试试：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">GET_CONFIG            → 正常返回配置</span><br><span class="line">GET_RECENT_COMMENTS   → 正常返回评论数据</span><br><span class="line">GET_COMMENTS          → 1001</span><br></pre></td></tr></table></figure><p>配置能读、数据能读，只有那一个事件不通——<strong>根本不是版本太旧，是前后端事件名对不上</strong>。</p><h2 id="第三步：干脆搬回自己机器"><a href="#第三步：干脆搬回自己机器" class="headerlink" title="第三步：干脆搬回自己机器"></a>第三步：干脆搬回自己机器</h2><p>既然域名被污染、版本又纠缠，索性不再依赖 Vercel。自己的服务器上跑一个容器就行：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker run -d --name twikoo \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  --env-file /root/twikoo.env \</span><br><span class="line">  -p 127.0.0.1:8082:8080 \</span><br><span class="line">  -v /mydata/twikoo/data:/app/data \</span><br><span class="line">  imaegoo/twikoo:latest</span><br></pre></td></tr></table></figure><p>几个要点：</p><ul><li>**只监听 <code>127.0.0.1</code>**，公网访问一律走 nginx 反代</li><li>数据库连接串放在 <code>--env-file</code> 里，文件权限 <code>600</code>，不要写在命令行（会进 shell history）</li><li>数据库还是用原来的 MongoDB Atlas，<strong>历史评论不用迁</strong>，连上就能读</li></ul><p><code>/root/twikoo.env</code>：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">MONGODB_URI</span>=mongodb+srv://用户:密码@集群/?retryWrites=<span class="literal">true</span>&amp;w=majority</span><br><span class="line"><span class="attr">TWIKOO_THROTTLE</span>=<span class="number">1000</span></span><br></pre></td></tr></table></figure><h2 id="第四步：nginx-反代到一个路径"><a href="#第四步：nginx-反代到一个路径" class="headerlink" title="第四步：nginx 反代到一个路径"></a>第四步：nginx 反代到一个路径</h2><p>不想再折腾子域名和证书，直接挂在主域名的路径下：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /api/twikoo &#123;</span><br><span class="line">    <span class="attribute">rewrite</span><span class="regexp"> ^/api/twikoo/?(.*)$</span> /<span class="variable">$1</span> <span class="literal">break</span>;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:8082;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>rewrite ... break</code> 那行是关键：容器的接口在根路径 <code>/</code> 上，请求进来是 <code>/api/twikoo</code>，必须把前缀削掉再转发，否则容器收到 <code>/api/twikoo</code> 会 404。</p><p>博客配置改成：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">twikoo:</span></span><br><span class="line">  <span class="attr">envId:</span> <span class="string">https://你的域名/api/twikoo</span></span><br></pre></td></tr></table></figure><p>好处是：<strong>不用动 DNS、不用单独申请证书</strong>，主域名有什么配置它就用什么。</p><h2 id="第五步：前端也得是-2-x"><a href="#第五步：前端也得是-2-x" class="headerlink" title="第五步：前端也得是 2.x"></a>第五步：前端也得是 2.x</h2><p>后端换好了，前端还停在 1.6.x，照样不通。但直接改版本号会撞墙：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">https://cdn.staticfile.org/twikoo/2.0.17/twikoo.all.min.js  → 404</span><br><span class="line">https://cdn.staticfile.org/twikoo/1.6.31/twikoo.all.min.js  → 200</span><br></pre></td></tr></table></figure><p>国内 CDN 只同步到 1.6.31，jsdelivr 这边又连不上。绕开的办法是从国内 npm 镜像把包下下来，<strong>放到自己站点里</strong>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -o <span class="built_in">source</span>/js/twikoo.all.min.js \</span><br><span class="line">  <span class="string">&quot;https://registry.npmmirror.com/twikoo/2.0.17/files/dist/twikoo.all.min.js&quot;</span></span><br></pre></td></tr></table></figure><p>然后在主题配置里覆盖这个脚本的地址（Butterfly 支持逐个插件指定 URL）：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">CDN:</span></span><br><span class="line">  <span class="attr">option:</span></span><br><span class="line">    <span class="attr">twikoo:</span> <span class="string">/js/twikoo.all.min.js</span></span><br></pre></td></tr></table></figure><p>顺带的好处：评论系统不再依赖任何外部 CDN，外部 CDN 出问题、域名被墙，跟我没关系了。</p><h2 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h2><table><thead><tr><th>项目</th><th>之前</th><th>现在</th></tr></thead><tbody><tr><td>评论接口</td><td><code>*.vercel.app</code>，国内不通</td><td><code>自己的域名/api/twikoo</code></td></tr><tr><td>前端脚本</td><td>第三方 CDN 1.6.x</td><td>本站托管 2.0.x</td></tr><tr><td>数据</td><td>Atlas</td><td>Atlas（不变，直接沿用）</td></tr><tr><td>部署平台</td><td>Vercel</td><td>自己的 Docker</td></tr></tbody></table><p>历史评论一条没丢——数据一直在数据库里，之前只是「接口调不通」，不是「数据没了」。</p><h2 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h2><p>这次真正花时间的不是部署，是<strong>被错误提示带偏</strong>。「请更新云函数」这句话让我在后端版本上兜了好几圈，而真实原因（事件名变了）藏在源码的一个常量定义里。</p><p>遇到报错，尤其是第三方封装过的报错，<strong>先去查错误码的真实定义</strong>，别信文案。</p>]]>
    </content>
    <id>https://cyk.pink/posts/5e83b06a.html</id>
    <link href="https://cyk.pink/posts/5e83b06a.html"/>
    <published>2026-10-27T07:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>博客的评论区一直在转圈，一直以为是「没人评论所以空的」。</p>
<p>直到有一天为了查别的问题去调评论接口，发现请求根本没回来：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">curl -X POST https://xxx.vercel.app/ → 超时</span><br></pre></td></tr></table></figure>

<h2 id="第一步：先看-DNS，别急着改代码"><a href="#第一步：先看-DNS，别急着改代码" class="headerlink" title="第一步：先看 DNS，别急着改代码"></a>第一步：先看 DNS，别急着改代码</h2><figure class="highlight plaintext"><table><tr><td]]>
    </summary>
    <title>评论区加载不出来？把 Twikoo 从 Vercel 搬回自己服务器</title>
    <updated>2026-10-27T07:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="性能优化" scheme="https://cyk.pink/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <category term="Nginx" scheme="https://cyk.pink/tags/Nginx/"/>
    <category term="HTTP2" scheme="https://cyk.pink/tags/HTTP2/"/>
    <category term="图床" scheme="https://cyk.pink/tags/%E5%9B%BE%E5%BA%8A/"/>
    <content>
      <![CDATA[<h2 id="先量化，再优化"><a href="#先量化，再优化" class="headerlink" title="先量化，再优化"></a>先量化，再优化</h2><p>重装之后顺手给站点做了一次体检，四个数字最扎眼：</p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>HTTPS</td><td>只有 HTTP&#x2F;1.1</td></tr><tr><td>gzip</td><td>只压 HTML，CSS&#x2F;JS 全裸奔</td></tr><tr><td>图片缓存头</td><td>没有</td></tr><tr><td>图床里没人引用的死图</td><td>40 个，占 6.5MB &#x2F; 8.9MB</td></tr></tbody></table><p>前三个都是 nginx 一行配置的事，最后一个是长期积累的垃圾。</p><h2 id="HTTP-2：一个连接跑完所有资源"><a href="#HTTP-2：一个连接跑完所有资源" class="headerlink" title="HTTP&#x2F;2：一个连接跑完所有资源"></a>HTTP&#x2F;2：一个连接跑完所有资源</h2><p>HTTP&#x2F;1.1 的问题是「一个连接同时只能处理一个请求」，浏览器只能靠开 6 个并发连接硬堆。页面有几十个资源的时候，排队是实打实的延迟。</p><p>开启只需要在 listen 后面加一个词：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">listen</span> <span class="number">443</span> ssl http2;</span><br></pre></td></tr></table></figure><p>验证别只看配置，要实际协商：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">echo</span> | openssl s_client -connect 127.0.0.1:443 -servername 你的域名 -alpn h2 2&gt;/dev/null \</span><br><span class="line">  | grep -i <span class="string">&quot;ALPN protocol&quot;</span></span><br></pre></td></tr></table></figure><p>看到 <code>ALPN protocol: h2</code> 才算真的开了。</p><h2 id="gzip：默认只压-HTML"><a href="#gzip：默认只压-HTML" class="headerlink" title="gzip：默认只压 HTML"></a>gzip：默认只压 HTML</h2><p>这是最容易忽略的一项。nginx 的 <code>gzip on;</code> 打开之后，<code>gzip_types</code> 的**默认值只有 <code>text/html</code>**——CSS、JS、JSON、XML 一个都不压。</p><p>Ubuntu 的默认配置里这一行是注释掉的，很多人以为开了 gzip 就万事大吉：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"><span class="attribute">gzip_vary</span> <span class="literal">on</span>;</span><br><span class="line"><span class="attribute">gzip_comp_level</span> <span class="number">6</span>;</span><br><span class="line"><span class="attribute">gzip_min_length</span> <span class="number">1024</span>;</span><br><span class="line"><span class="attribute">gzip_types</span> text/plain text/css text/xml application/json application/javascript</span><br><span class="line">           application/xml application/xml+rss image/svg+xml;</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -s -o /dev/null -H <span class="string">&quot;Accept-Encoding: gzip&quot;</span> \</span><br><span class="line">  -w <span class="string">&quot;%&#123;size_download&#125; bytes\n&quot;</span> https://你的域名/css/xxx.css</span><br><span class="line">curl -sI -H <span class="string">&quot;Accept-Encoding: gzip&quot;</span> https://你的域名/css/xxx.css | grep -i content-encoding</span><br></pre></td></tr></table></figure><p>我这个站上的 CSS 压完大概剩三成。</p><h2 id="缓存头：让重复访问变成零请求"><a href="#缓存头：让重复访问变成零请求" class="headerlink" title="缓存头：让重复访问变成零请求"></a>缓存头：让重复访问变成零请求</h2><p>图片走 nginx 反代到对象存储，响应里只有 ETag 和 Last-Modified，<strong>没有 Cache-Control</strong>——意味着浏览器每次都要回源确认一遍。图多的页面，读者翻两页就能感觉到。</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /cykblog/ &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:9000;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public, max-age=2592000&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>静态资源同理：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~* \.(?:css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2?|ttf|eot)$</span> &#123;</span><br><span class="line">    <span class="attribute">expires</span> <span class="number">30d</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public&quot;</span>;</span><br><span class="line">    <span class="attribute">try_files</span> <span class="variable">$uri</span> =<span class="number">404</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意一个 add_header 的坑：<strong>location 里一旦写了 add_header，server 层的 add_header 就不再继承</strong>。所以安全响应头要在每个自己写了 header 的 location 里重复一遍，否则 <code>/cykblog/</code> 会漏掉。</p><h2 id="server-tokens-和安全头"><a href="#server-tokens-和安全头" class="headerlink" title="server_tokens 和安全头"></a>server_tokens 和安全头</h2><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">server_tokens</span> <span class="literal">off</span>;</span><br><span class="line"><span class="attribute">add_header</span> X-Content-Type-Options <span class="string">&quot;nosniff&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> X-Frame-Options <span class="string">&quot;SAMEORIGIN&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Referrer-Policy <span class="string">&quot;strict-origin-when-cross-origin&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Strict-Transport-Security <span class="string">&quot;max-age=2592000&quot;</span> always;</span><br></pre></td></tr></table></figure><p>关掉版本号不是为了「安全多少」，是为了别主动告诉扫描器「我是 nginx 1.24.0，你可以去查这个版本的 CVE」。</p><p>HSTS 我一开始只给了 30 天，没设 <code>preload</code>。这个头一旦下发，浏览器在有效期内会强制走 HTTPS——证书出问题时网站会直接打不开，所以先短后长比较稳。</p><h2 id="第三方-CDN-的两个教训"><a href="#第三方-CDN-的两个教训" class="headerlink" title="第三方 CDN 的两个教训"></a>第三方 CDN 的两个教训</h2><p><strong>第一个：图标字体加载失败。</strong> 页面里引用了一个国内 CDN 的 iconfont，报 <code>net::ERR_CONNECTION_CLOSED</code>。在服务器上直接抓那个文件是 200，说明 CDN 本身没问题，是我本地到它的链路被掐了。</p><p>解决办法不是换 CDN，是把字体<strong>搬到自己的域名下</strong>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -o iconfont.css <span class="string">&quot;https://原CDN/iconfont.css&quot;</span></span><br><span class="line">curl -o iconfont.woff2 <span class="string">&quot;https://原CDN/iconfont.woff2&quot;</span></span><br></pre></td></tr></table></figure><p>CSS 里用的是相对路径（<code>url(&#39;iconfont.woff2&#39;)</code>），放同一个目录就能自动找到，不用改。</p><p><strong>第二个：统计脚本报 ERR_BLOCKED_BY_ORB。</strong> 一个免费访问量统计服务，请求失败后还会挂两分钟才超时，直接把首屏拖慢。关掉，换成 Cloudflare 的 Web Analytics（无 cookie、脚本小、国内可达）。</p><h2 id="图床清理：40-个死图占-73-空间"><a href="#图床清理：40-个死图占-73-空间" class="headerlink" title="图床清理：40 个死图占 73% 空间"></a>图床清理：40 个死图占 73% 空间</h2><p>把桶里所有对象和「页面里实际引用到的」做了一次差集，结果很难看：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">对象总数 80，被引用 51，无人引用 40，占用 6.5MB / 8.9MB</span><br></pre></td></tr></table></figure><p>最大的一个是一张 2560×1440 的 PNG 壁纸，<strong>4MB</strong>——而且同一张图我早就转过 webp（104KB），只是忘了删 PNG，也没在用。</p><p>清理时踩了一个坑：<strong>用文件名直接 grep HTML 会漏</strong>。中文名和带反引号的文件名在 HTML 里是 URL 编码的：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">磁盘：`scroll-view`滑动布局.webp</span><br><span class="line">HTML：%60scroll-view%60%E6%BB%91%E5%8A%A8%E5%B8%83%E5%B1%80.webp</span><br></pre></td></tr></table></figure><p>第一次按文件名匹配，把它们当成「无人引用」删了，结果 12 张正在用的图直接 404。还好删之前打了包，全部恢复。</p><p>正确的做法是<strong>先解码再比对</strong>，而且要同时扫 html &#x2F; css &#x2F; js 三类文件：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> m <span class="keyword">in</span> re.findall(<span class="string">r&#x27;cykblog/([^&quot;\&#x27;\s)&lt;&gt;?#]+)&#x27;</span>, txt):</span><br><span class="line">    refs.add(urllib.parse.unquote(m))</span><br></pre></td></tr></table></figure><p>改完之后桶从 8.9MB 降到 3.0MB，引用完整性校验为 0 缺失。</p><h2 id="上传链路：把规范写进工具"><a href="#上传链路：把规范写进工具" class="headerlink" title="上传链路：把规范写进工具"></a>上传链路：把规范写进工具</h2><p>以前传图是「拖进去就完事」，于是文件名长这样：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">image-20240729225711008.png</span><br><span class="line">wallhaven-6d61g4_2560x1440.png</span><br><span class="line">`swiper`纵向轮播1.webp</span><br></pre></td></tr></table></figure><p>现在用 PicGo 传，把规则配死：</p><ul><li><strong>路径</strong>：<code>posts/&#123;年&#125;/&#123;月&#125;/</code>（对象存储里的目录）</li><li><strong>文件名</strong>：<code>&#123;y&#125;&#123;m&#125;&#123;d&#125;-&#123;rand:6&#125;</code>，例如 <code>20261027-a3f9c2.webp</code></li></ul><p>有个细节值得记一下：PicGo 的 MinIO 插件里，<code>pathFormat</code> 只支持 <code>&#123;bucket&#125;</code> 一个占位符，**不支持 <code>&#123;年&#125;/&#123;月&#125;</code>**。如果照着别的插件文档填，最后会生成一个字面量路径 <code>posts/&#123;年&#125;/&#123;月&#125;/</code>，对象存的路径和返回的 URL 对不上，图片必然 404。</p><h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>改动都不大，但叠加起来：</p><ul><li>资源请求从「串行排队」变成「一个连接并发」</li><li>CSS&#x2F;JS 传输量降到三成左右</li><li>重复访问的图片基本不再回源</li><li>图床体积减少 66%</li><li>首屏少了一个会挂两分钟的第三方请求</li></ul><p>都是教科书里的东西，只是以前觉得「博客而已，无所谓」。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9a1c47d2.html</id>
    <link href="https://cyk.pink/posts/9a1c47d2.html"/>
    <published>2026-10-27T03:50:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="先量化，再优化"><a href="#先量化，再优化" class="headerlink" title="先量化，再优化"></a>先量化，再优化</h2><p>重装之后顺手给站点做了一次体检，四个数字最扎眼：</p>
<table>
<thead>
<tr>
<th>指标</th>
<th>数值</th>
</tr>
</thead>
<tbody><tr>
<td>HTTPS</td>
<td>只有 HTTP&#x2F;1.1</td>
</tr>
<tr>
<td>gzip</td>
<td>只压 HTML，CSS&#x2F;JS 全裸奔</td>
</tr>
<tr>
<td>图片缓存头</td>
<td>没有</td>
</tr>
<tr>
<td>图床里没人引用的死图</td>
<td>40 个，占 6.5MB &#x2F; 8.9MB</td>
</tr>
</tbody></table>
<p>前三个都是 nginx 一行配置的事，最后一个是长期积累的垃圾。</p>
<h2 id="HTTP-2：一个连接跑完所有资源"><a]]>
    </summary>
    <title>静态博客的传输层优化：HTTP/2、gzip 和缓存头</title>
    <updated>2026-10-27T03:50:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="Nginx" scheme="https://cyk.pink/tags/Nginx/"/>
    <category term="运维" scheme="https://cyk.pink/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="SSH" scheme="https://cyk.pink/tags/SSH/"/>
    <category term="fail2ban" scheme="https://cyk.pink/tags/fail2ban/"/>
    <content>
      <![CDATA[<h2 id="重装之后的第一件事不是装软件"><a href="#重装之后的第一件事不是装软件" class="headerlink" title="重装之后的第一件事不是装软件"></a>重装之后的第一件事不是装软件</h2><p>系统重装完，<code>ssh root@ip</code> 一通，密码进去——<strong>这一步就已经错了</strong>。</p><p>上一台机器出事，本质就是「密码可以被人猜」。所以这次第一件事是：<strong>把密码登录这条路彻底关掉</strong>，改成密钥。</p><h2 id="SSH-换密钥"><a href="#SSH-换密钥" class="headerlink" title="SSH 换密钥"></a>SSH 换密钥</h2><p>本地生成一对 ed25519（比 RSA 短、比 RSA 快、强度还更高）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -f ~/.ssh/cyk_server -N <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>把公钥追加到服务器的 <code>~/.ssh/authorized_keys</code>，权限必须是这套：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 700 ~/.ssh</span><br><span class="line"><span class="built_in">chmod</span> 600 ~/.ssh/authorized_keys</span><br></pre></td></tr></table></figure><p>然后写一份加固配置，放在 <code>sshd_config.d/</code> 下（注意文件名要排在云厂商的默认配置<strong>前面</strong>，因为 sshd 是「先读到的生效」）：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">PasswordAuthentication</span> <span class="literal">no</span></span><br><span class="line">KbdInteractiveAuthentication <span class="literal">no</span></span><br><span class="line">PermitRootLogin prohibit-password</span><br><span class="line">MaxAuthTries <span class="number">3</span></span><br></pre></td></tr></table></figure><p><code>prohibit-password</code> 的意思不是「允许 root 用密码」，而是「root 只能用密钥」——等于把 root 远程登录变相禁掉了。</p><p>改完先 <code>sshd -t</code> 验证语法，再 reload。<strong>验证前不要关当前窗口</strong>，不然配错了就真的进不去了。</p><p>现在登录长这样：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh cyk-server</span><br></pre></td></tr></table></figure><p><code>.ssh/config</code> 里配好别名，不用记 IP、不用带 <code>-i</code>。</p><h2 id="端口从-12-个砍到-3-个"><a href="#端口从-12-个砍到-3-个" class="headerlink" title="端口从 12 个砍到 3 个"></a>端口从 12 个砍到 3 个</h2><p>老机器上开了一堆端口：MySQL、Redis、Nacos、MinIO 控制台……每个都是当年「临时测一下」留下的。</p><p>新机器只留：</p><table><thead><tr><th>端口</th><th>用途</th></tr></thead><tbody><tr><td>22</td><td>SSH</td></tr><tr><td>80</td><td>HTTP（301 跳 HTTPS）</td></tr><tr><td>443</td><td>HTTPS</td></tr></tbody></table><p>图床的 9000 和 9090 全部改成只监听 <code>127.0.0.1</code>，需要的时候用 SSH 隧道临时打开：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh -L 9000:127.0.0.1:9000 -L 9090:127.0.0.1:9090 ubuntu@ip -N</span><br></pre></td></tr></table></figure><p>隧道比开放端口好在：<strong>它要求先有 SSH 密钥</strong>。开端口是「对全世界开一扇门」，隧道是「拿着钥匙才给你开」。</p><h2 id="fail2ban"><a href="#fail2ban" class="headerlink" title="fail2ban"></a>fail2ban</h2><p>老机器被打了 4466 次，一次告警都没有。这次装上：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt install fail2ban</span><br></pre></td></tr></table></figure><p>默认策略是「10 分钟内失败 5 次封 10 分钟」——10 分钟太短了，换个时间再来就跟没封一样。改成封一天：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /etc/fail2ban/jail.local</span></span><br><span class="line"><span class="section">[DEFAULT]</span></span><br><span class="line"><span class="attr">bantime</span> = <span class="number">86400</span></span><br><span class="line"><span class="attr">findtime</span> = <span class="number">600</span></span><br><span class="line"><span class="attr">maxretry</span> = <span class="number">5</span></span><br></pre></td></tr></table></figure><p>有个副作用要记住：<strong>自己输错 5 次也会被封一天</strong>。被 DROP 掉的表现是「连接超时」而不是「拒绝连接」，很容易误判成网络问题。解封：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">fail2ban-client <span class="built_in">set</span> sshd unbanip 你的IP</span><br></pre></td></tr></table></figure><h2 id="服务不再直接对外"><a href="#服务不再直接对外" class="headerlink" title="服务不再直接对外"></a>服务不再直接对外</h2><p>图床（MinIO）收到内网，对外只通过 nginx 反代一个路径：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /cykblog/ &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:9000;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public, max-age=2592000&quot;</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里有个坑：前缀 location 必须写 <code>^~</code>。否则下面那条「静态资源后缀」的正则 location 会优先匹配，把 <code>/cykblog/xxx.webp</code> 当成磁盘文件去找，直接 404。</p><p>文件管理（FileBrowser）也收到内网，但因为它有网页界面、还要长期对外，所以在 nginx 前面又加了一道 Basic Auth：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /files/ &#123;</span><br><span class="line">    <span class="attribute">auth_basic</span> <span class="string">&quot;Restricted&quot;</span>;</span><br><span class="line">    <span class="attribute">auth_basic_user_file</span> /etc/nginx/.htpasswd;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:8081;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>密码文件用 openssl 直接生成，不用额外装包：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;user:%s\n&#x27;</span> <span class="string">&quot;<span class="subst">$(openssl passwd -apr1 &#x27;你的密码&#x27;)</span>&quot;</span> &gt; /etc/nginx/.htpasswd</span><br></pre></td></tr></table></figure><p>现在访问 <code>/files/</code> 是两层：先进 nginx 的认证，再进 FileBrowser 自己的登录页。这套软件官方已经停止维护了，多一层能挡住「拿已知漏洞直接打」的自动化扫描。</p><h2 id="证书"><a href="#证书" class="headerlink" title="证书"></a>证书</h2><p>Let’s Encrypt，certbot 自动续期，90 天一轮。装完确认一下定时任务在：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">systemctl list-timers | grep certbot</span><br></pre></td></tr></table></figure><h2 id="一个反直觉的点"><a href="#一个反直觉的点" class="headerlink" title="一个反直觉的点"></a>一个反直觉的点</h2><p>加固做完之后，日常使用<strong>几乎没有变麻烦</strong>：</p><ul><li>SSH 还是敲一句，只是不用输密码了</li><li>图床上传还是拖文件，只是先开个隧道（或者用 PicGo 配好一次）</li><li>网页访问完全没区别</li></ul><p>真正变麻烦的只有一件事：<strong>换新设备的时候要加一次公钥</strong>。三十秒的事。</p><p>反过来想，那台老机器省下的这三十秒，代价是两周的 root 权限在别人手里。</p>]]>
    </content>
    <id>https://cyk.pink/posts/3d5f8b17.html</id>
    <link href="https://cyk.pink/posts/3d5f8b17.html"/>
    <published>2026-10-27T01:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="重装之后的第一件事不是装软件"><a href="#重装之后的第一件事不是装软件" class="headerlink" title="重装之后的第一件事不是装软件"></a>重装之后的第一件事不是装软件</h2><p>系统重装完，<code>ssh root@ip</code> 一通，密码进去——<strong>这一步就已经错了</strong>。</p>
<p>上一台机器出事，本质就是「密码可以被人猜」。所以这次第一件事是：<strong>把密码登录这条路彻底关掉</strong>，改成密钥。</p>
<h2 id="SSH-换密钥"><a href="#SSH-换密钥" class="headerlink" title="SSH 换密钥"></a>SSH 换密钥</h2><p>本地生成一对 ed25519（比 RSA 短、比 RSA 快、强度还更高）：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh-keygen -t]]>
    </summary>
    <title>重装只是开始：把公网服务器从裸奔状态拉回来</title>
    <updated>2026-10-27T01:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="服务器" scheme="https://cyk.pink/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    <category term="入侵复盘" scheme="https://cyk.pink/tags/%E5%85%A5%E4%BE%B5%E5%A4%8D%E7%9B%98/"/>
    <category term="运维" scheme="https://cyk.pink/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>一直挺得意那台轻量服务器——2 核 4G，跑着博客、图床、一个文件管理，两年没出过事。</p><p>10 月 26 号晚上翻图床的时候发现不对劲：桶里多了几个从来没上传过的文件，名字是 <code>shell.php</code>、<code>.htaccess</code>，还有一个叫 <code>ahk_probe</code> 的东西。第一反应是自己手滑，第二反应是——我根本没开过 PHP。</p><h2 id="往下挖"><a href="#往下挖" class="headerlink" title="往下挖"></a>往下挖</h2><p>先看的不是网站，是进程。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ps auxf | grep -v <span class="string">&#x27;\[&#x27;</span> </span><br></pre></td></tr></table></figure><p>排在最上面的几个进程看着很正常，名字是系统自带的那种。但路径不对：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/dev/shm/.collector/collector.d</span><br></pre></td></tr></table></figure><p><code>/dev/shm</code> 是内存盘，重启就清空，正常服务不会往这儿放东西。<code>collector.d</code> 这个名字起得很「正经」，伪装成日志收集器——这种命名习惯不是随手写的，是有人专门研究过怎么不被人一眼看出来。</p><p>再看 <code>/root/.ssh/</code>，<code>authorized_keys</code> 里多了一把不属于我的公钥。也就是说：<strong>对方不需要密码也能随时回来。</strong></p><h2 id="溯源：入口在哪"><a href="#溯源：入口在哪" class="headerlink" title="溯源：入口在哪"></a>溯源：入口在哪</h2><p>翻 nginx 和 MinIO 的日志，链条很清楚：</p><ol><li>图床的 9000 端口<strong>直接对着公网开着</strong></li><li>MinIO 用的是默认凭据 <code>minioadmin / minioadmin</code>（建的时候图省事，一直没改）</li><li>对方扫到 9000，用默认口令直接进来了</li><li>通过 MinIO 的写入权限往桶里丢 web shell 和探测脚本</li><li>拿到执行权限后提权、装后门、留公钥</li></ol><p>整个过程没有任何「高难度操作」。用的是<strong>出厂默认密码</strong>和<strong>不必要对外开放的端口</strong>。</p><p>顺带在 <code>auth.log</code> 里翻了 SSH 爆破记录，三个来源，都是同一批端口、同一套字典：</p><table><thead><tr><th>来源地址</th><th>归属</th><th>行为</th></tr></thead><tbody><tr><td><code>51.222.47.156</code></td><td>境外（OVH 地址段）</td><td>单 IP 尝试登录 <strong>4466 次</strong></td></tr><tr><td><code>106.53.117.97</code></td><td>中国·广东·广州（腾讯云）</td><td>扫描同一批端口</td></tr><tr><td><code>182.134.27.185</code></td><td>中国·四川·宜宾（电信）</td><td>扫描同一批端口</td></tr></tbody></table><p>没什么好洗的，就是三个贱人：</p><ul><li><code>51.222.47.156</code>（境外 OVH 段）—— 一个 IP 硬试了 4466 次</li><li><code>106.53.117.97</code>（广东广州，腾讯云）</li><li><code>182.134.27.185</code>（四川宜宾，电信）</li></ul><p>租台云主机来扫端口是贱人，拿别人的机器当跳板更是贱人。这三个地址我记在这儿了，哪天想起来了就翻出来看一眼。</p><p>骂完还得防。真正让它们无从下手的是「密码登录关掉」和「端口不对外」。</p><h2 id="止损"><a href="#止损" class="headerlink" title="止损"></a>止损</h2><p>发现后没做「清理」这种操作，直接按事故处理：</p><ol><li><strong>先关机</strong>，切断对方的操作通道——清理是在跟对手赛跑，没必要</li><li>关机后<strong>打快照</strong>，把现场固定下来（这台机器上的一切都可能是证据）</li><li>重启挂个临时环境，只对自己的 IP 开放，把<strong>数据捞出来</strong></li><li>数据核对无误后，<strong>整机重装</strong></li></ol><p>这里有个判断很关键：<strong>不要试图「删掉后门」然后继续用</strong>。一个拿到过 root 的人，可以在任何地方留东西——内核模块、cron、systemd、甚至固件。你不可能穷举。</p><p>唯一可靠的处置是重装。</p><h2 id="数据是怎么保住的"><a href="#数据是怎么保住的" class="headerlink" title="数据是怎么保住的"></a>数据是怎么保住的</h2><p>图床数据在对象存储的桶里，站点是纯静态文件，都落在数据盘上。把这两个目录打包：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">tar czf backup.tar.gz /数据目录/minio/data /var/www</span><br></pre></td></tr></table></figure><p>打完校验 SHA256，传到本地再核一遍。中间出过一次传输损坏，第二次才拿到一致的哈希——<strong>这一步千万不能省</strong>，一个坏包等于没备份。</p><h2 id="教训"><a href="#教训" class="headerlink" title="教训"></a>教训</h2><p>三条，按严重程度排：</p><table><thead><tr><th>问题</th><th>后果</th></tr></thead><tbody><tr><td><strong>默认密码</strong>（minioadmin&#x2F;minioadmin）</td><td>等于没设密码，扫描器字典第一条就是它</td></tr><tr><td><strong>端口对公网敞开</strong>（12 个端口）</td><td>每个端口都是一个入口，图床根本不需要公网直连</td></tr><tr><td><strong>root 密码复用且偏弱</strong></td><td>一旦进来，横向到系统层毫无阻力</td></tr></tbody></table><p>还有一个隐性的：<strong>我从来没看过那台机器的日志</strong>。4466 次爆破，失败了 4466 次，没有一次触发告警，因为压根没装 fail2ban。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>这次没人身损失，数据也都回来了。但那种「自己的机器上跑着别人的进程」的感觉，比丢数据更让人不舒服。</p><p>后面几篇会写重装之后做了什么——SSH 换密钥、端口砍到 3 个、fail2ban、图床收回内网、评论系统搬回自己机器。都是很基础的东西，基础到出事之前我一直觉得「没必要」。</p>]]>
    </content>
    <id>https://cyk.pink/posts/7c2a91e4.html</id>
    <link href="https://cyk.pink/posts/7c2a91e4.html"/>
    <published>2026-10-26T14:40:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>一直挺得意那台轻量服务器——2 核 4G，跑着博客、图床、一个文件管理，两年没出过事。</p>
<p>10 月 26 号晚上翻图床的时候发现不对劲：桶里多了几个从来没上传过的文件，名字是 <code>shell.php</code>、<code>.htaccess</code>，还有一个叫 <code>ahk_probe</code> 的东西。第一反应是自己手滑，第二反应是——我根本没开过 PHP。</p>
<h2 id="往下挖"><a href="#往下挖" class="headerlink" title="往下挖"></a>往下挖</h2><p>先看的不是网站，是进程。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ps auxf | grep -v <span]]>
    </summary>
    <title>服务器被拿去当肉鸡：一次入侵的完整复盘</title>
    <updated>2026-10-26T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="工具笔记" scheme="https://cyk.pink/categories/%E5%B7%A5%E5%85%B7%E7%AC%94%E8%AE%B0/"/>
    <category term="工具" scheme="https://cyk.pink/tags/%E5%B7%A5%E5%85%B7/"/>
    <category term="终端" scheme="https://cyk.pink/tags/%E7%BB%88%E7%AB%AF/"/>
    <category term="Shell" scheme="https://cyk.pink/tags/Shell/"/>
    <category term="命令行" scheme="https://cyk.pink/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
    <content>
      <![CDATA[<p>oh-my-posh 是个跨平台的终端提示符美化工具，支持 PowerShell、bash、zsh、fish、cmd。它用 JSON 配置文件定义提示符长什么样，可以显示当前目录、git 分支、执行时间、电池、天气、各种编程语言版本等。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Windows 上最方便的是用 winget：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh</span><br></pre></td></tr></table></figure><p>装完会多出一个 <code>oh-my-posh.exe</code>。macOS 和 Linux 可以用 Homebrew：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">brew install jandedobbeleer/oh-my-posh/oh-my-posh</span><br></pre></td></tr></table></figure><p>或者直接用官方安装脚本：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -s https://ohmyposh.dev/install.sh | bash</span><br></pre></td></tr></table></figure><h2 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h2><p>oh-my-posh 的很多主题用到 Nerd Font 图标。如果看到方块或乱码，说明当前字体没有这些字形。</p><p>推荐装 MesloLGM Nerd Font 或 Cascadia Code PL。Windows Terminal 里在设置 → 外观 → 字体里选上。VS Code 终端同样要在 <code>settings.json</code> 里配 <code>terminal.integrated.fontFamily</code>。</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><p>根据你的 shell 加一句启动命令。</p><p>PowerShell（加到 <code>$PROFILE</code>）：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><p>bash（加到 <code>~/.bashrc</code>）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(oh-my-posh init bash)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>zsh（加到 <code>~/.zshrc</code>）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(oh-my-posh init zsh)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>fish（加到 <code>~/.config/fish/config.fish</code>）：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">oh-my-posh init fish | source</span><br></pre></td></tr></table></figure><h2 id="换主题"><a href="#换主题" class="headerlink" title="换主题"></a>换主题</h2><p>初始化时可以直接指定主题：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$env:POSH_THEMES_PATH</span>\jandedobbeleer.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><p>官方内置了很多主题，装完后主题文件一般在：</p><ul><li>Windows：<code>%LOCALAPPDATA%\Programs\oh-my-posh\themes\</code></li><li>macOS&#x2F;Linux：<code>~/.cache/oh-my-posh/themes/</code></li></ul><p>想试效果可以临时运行：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> print primary <span class="literal">--config</span> <span class="string">&quot;C:\path\to\theme.omp.json&quot;</span></span><br></pre></td></tr></table></figure><h2 id="自己改配置"><a href="#自己改配置" class="headerlink" title="自己改配置"></a>自己改配置</h2><p>主题就是 JSON。一个最小结构长这样：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;blocks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prompt&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;alignment&quot;</span><span class="punctuation">:</span> <span class="string">&quot;left&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;segments&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;path&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;plain&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#26C6DA&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;&#123; .Path &#125;&#125; &quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;plain&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#FFEB3B&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot;(&#123;&#123; .HEAD &#125;&#125;)&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>segment 类型很多：path、git、executiontime、battery、node、python、go、kubectl……官方文档有完整列表。</p><h2 id="多台机器同步"><a href="#多台机器同步" class="headerlink" title="多台机器同步"></a>多台机器同步</h2><p>把主题文件放到 git 仓库或网盘里同步，初始化时指向固定路径。或者用环境变量 <code>POSH_THEME</code> 指向配置，这样换机器只需改一个变量。</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$env:POSH_THEME</span> = <span class="string">&quot;C:\Users\codem\.config\oh-my-posh\mytheme.omp.json&quot;</span></span><br><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="variable">$env:POSH_THEME</span> | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><h2 id="值不值得装"><a href="#值不值得装" class="headerlink" title="值不值得装"></a>值不值得装</h2><p>如果你每天大量时间在终端里，一个信息丰富、一眼能看清上下文状态的提示符确实能省不少事。oh-my-posh 配置一次，跨 shell 复用，维护成本不高。</p>]]>
    </content>
    <id>https://cyk.pink/posts/85891fe1.html</id>
    <link href="https://cyk.pink/posts/85891fe1.html"/>
    <published>2026-08-05T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>oh-my-posh 是个跨平台的终端提示符美化工具，支持 PowerShell、bash、zsh、fish、cmd。它用 JSON 配置文件定义提示符长什么样，可以显示当前目录、git 分支、执行时间、电池、天气、各种编程语言版本等。</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Windows 上最方便的是用 winget：</p>
<figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh</span><br></pre></td></tr></table></figure>

<p>装完会多出一个 <code>oh-my-posh.exe</code>。macOS 和 Linux 可以用 Homebrew：</p>
<figure class="highlight]]>
    </summary>
    <title>oh-my-posh：一个好看的终端提示符怎么装</title>
    <updated>2026-08-05T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Python" scheme="https://cyk.pink/tags/Python/"/>
    <category term="DOCX" scheme="https://cyk.pink/tags/DOCX/"/>
    <category term="LaTeX" scheme="https://cyk.pink/tags/LaTeX/"/>
    <category term="Markdown" scheme="https://cyk.pink/tags/Markdown/"/>
    <category term="工具" scheme="https://cyk.pink/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<p>DOCX Pipeline 是个把 Markdown 转成中文 DOCX 的命令行工具。双后端：Pure Python 零外部依赖，Pandoc 质量更高。v1.2.0 补上了纯 Python 后端对 LaTeX 数学公式的支持，写论文、出报告时不用硬装 Pandoc。</p><p>下面先说怎么用，再简单提实现。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Python 3.10 以上：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip install git+https://github.com/redamancy231-create/docx-pipeline.git</span><br></pre></td></tr></table></figure><p>核心依赖会一起装好：<code>python-docx</code>、<code>PyYAML</code>、<code>click</code>、<code>Pillow</code>。Pandoc 和 mermaid-cli 是可选的，后面按需安装。</p><h2 id="一分钟上手"><a href="#一分钟上手" class="headerlink" title="一分钟上手"></a>一分钟上手</h2><h3 id="1-初始化项目"><a href="#1-初始化项目" class="headerlink" title="1. 初始化项目"></a>1. 初始化项目</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline init --project-dir ./my-doc --template report --name <span class="string">&quot;技术报告&quot;</span></span><br></pre></td></tr></table></figure><p>执行完会在 <code>./my-doc</code> 下生成 <code>project.yaml</code>，里面包含页面、字体、路径、后端开关等所有配置。</p><p>四个内置模板：</p><table><thead><tr><th>模板</th><th>场景</th><th>默认后端</th></tr></thead><tbody><tr><td><code>default</code></td><td>通用中文文档</td><td>Pure Python</td></tr><tr><td><code>academic</code></td><td>论文&#x2F;学位论文</td><td>Pure Python</td></tr><tr><td><code>report</code></td><td>技术报告，带目录和 Mermaid</td><td>Pandoc</td></tr><tr><td><code>strategy</code></td><td>量化策略文档</td><td>Pure Python</td></tr></tbody></table><h3 id="2-写-Markdown"><a href="#2-写-Markdown" class="headerlink" title="2. 写 Markdown"></a>2. 写 Markdown</h3><p>模板会给你一个入口文件路径，默认在 <code>./md/main.md</code>。直接写普通 Markdown 就行，公式用 <code>$...$</code> 行内、<code>$$...$$</code> 独立：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="section">title: 示例报告</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># 正文</span></span><br><span class="line"></span><br><span class="line">考虑正弦信号的采样定理：</span><br><span class="line"></span><br><span class="line">$$f<span class="emphasis">_s \geq 2 f_</span>&#123;\max&#125;$$</span><br><span class="line"></span><br><span class="line">若采样率 $f<span class="emphasis">_s = 100\,\text&#123;Hz&#125;$，则奈奎斯特频率为 $f_</span>N = f<span class="emphasis">_s/2 = 50\,\text&#123;Hz&#125;$。</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## 表格</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">| 参数 | 值 |</span></span><br><span class="line"><span class="emphasis">|------|-----|</span></span><br><span class="line"><span class="emphasis">| 采样间隔 | 0.01 s |</span></span><br><span class="line"><span class="emphasis">| 窗长 | 21 |</span></span><br></pre></td></tr></table></figure><h3 id="3-转换"><a href="#3-转换" class="headerlink" title="3. 转换"></a>3. 转换</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ./my-doc</span><br><span class="line">docx-pipeline convert --config ./project.yaml</span><br></pre></td></tr></table></figure><p>输出路径由 <code>project.yaml</code> 里的 <code>paths.docx_output</code> 决定，默认 <code>./output/doc.docx</code>。</p><p>想换输出文件：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline convert --config ./project.yaml --output ./out/报告.docx</span><br></pre></td></tr></table></figure><p>只想看会不会报错，不生成文件：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline convert --config ./project.yaml --dry-run</span><br></pre></td></tr></table></figure><h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查看配置摘要</span></span><br><span class="line">docx-pipeline info --config ./project.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 校验配置合法性</span></span><br><span class="line">docx-pipeline validate --config ./project.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 强制用 Pure Python 后端</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --method pure</span><br><span class="line"></span><br><span class="line"><span class="comment"># 强制用 Pandoc 后端</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --method pandoc</span><br><span class="line"></span><br><span class="line"><span class="comment"># 传额外参数给 pandoc</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --pandoc-args <span class="string">&quot;--number-sections&quot;</span></span><br></pre></td></tr></table></figure><p><code>init</code> 和 <code>convert</code> 是日常使用最多的两个命令。</p><h2 id="配置重点"><a href="#配置重点" class="headerlink" title="配置重点"></a>配置重点</h2><p><code>project.yaml</code> 是全部行为的入口。几个常见调整：</p><p><strong>改输入输出路径</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">paths:</span></span><br><span class="line">  <span class="attr">md_source:</span> <span class="string">&quot;./chapters/main.md&quot;</span></span><br><span class="line">  <span class="attr">docx_output:</span> <span class="string">&quot;./output/报告.docx&quot;</span></span><br></pre></td></tr></table></figure><p><strong>改字体</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">fonts:</span></span><br><span class="line">  <span class="attr">east_asian:</span> <span class="string">&quot;微软雅黑&quot;</span></span><br><span class="line">  <span class="attr">latin:</span> <span class="string">&quot;Times New Roman&quot;</span></span><br></pre></td></tr></table></figure><p>字体必须在系统里已安装，否则 Word 会回退默认字体。</p><p><strong>页面和缩进</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">page:</span></span><br><span class="line">  <span class="attr">size:</span> <span class="string">&quot;A4&quot;</span></span><br><span class="line">  <span class="attr">orientation:</span> <span class="string">&quot;portrait&quot;</span></span><br><span class="line">  <span class="attr">margins:</span></span><br><span class="line">    <span class="attr">top:</span> <span class="number">2.54</span></span><br><span class="line">    <span class="attr">bottom:</span> <span class="number">2.54</span></span><br><span class="line">    <span class="attr">left:</span> <span class="number">3.18</span></span><br><span class="line">    <span class="attr">right:</span> <span class="number">3.18</span></span><br><span class="line"></span><br><span class="line"><span class="attr">styles:</span></span><br><span class="line">  <span class="attr">paragraph:</span></span><br><span class="line">    <span class="attr">line_spacing:</span> <span class="number">1.15</span></span><br><span class="line">    <span class="attr">first_line_indent:</span> <span class="number">0.0</span>   <span class="comment"># cm，学术论文可设 0.74 左右</span></span><br></pre></td></tr></table></figure><p><strong>后端开关</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">pandoc:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span>          <span class="comment"># true 启用 Pandoc</span></span><br><span class="line">  <span class="attr">extra_args:</span> []</span><br><span class="line">  <span class="attr">reference_docx:</span> <span class="string">&quot;&quot;</span>      <span class="comment"># Pandoc 参考文档路径</span></span><br></pre></td></tr></table></figure><p><code>enabled: false</code> 时走 Pure Python，LaTeX 公式由 v1.2.0 新增的 <code>latex2mathml</code> → MathML → OMML 桥转换。<code>enabled: true</code> 时走 Pandoc，公式渲染质量更好，但需要系统里有 pandoc。</p><h2 id="后端怎么选"><a href="#后端怎么选" class="headerlink" title="后端怎么选"></a>后端怎么选</h2><ul><li><strong>不想装 pandoc</strong> → Pure Python，一个 Python 环境就够了，公式也能转。</li><li><strong>要代码语法高亮、复杂表格、章节自动编号</strong> → Pandoc。</li><li><strong>想精确控制中文字体、缩进、行距</strong> → Pure Python 更直接。</li></ul><p>Auto 模式（默认）会先看 <code>pandoc.enabled</code>，再看 pandoc 是否在 PATH 里，能 pandoc 就 pandoc，不能就自动降级到 Pure Python。</p><h2 id="Mermaid-图表"><a href="#Mermaid-图表" class="headerlink" title="Mermaid 图表"></a>Mermaid 图表</h2><p><code>report</code> 模板默认开启 Mermaid。需要 Node.js 和 mermaid-cli：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g @mermaid-js/mermaid-cli</span><br></pre></td></tr></table></figure><p>Markdown 里写：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="code">```mermaid</span></span><br><span class="line"><span class="code">flowchart LR</span></span><br><span class="line"><span class="code">    A[输入] --&gt; B[处理] --&gt; C[输出]</span></span><br><span class="line"><span class="code">```</span></span><br></pre></td></tr></table></figure><p>转换时会自动渲染成 PNG 嵌入 DOCX。渲染失败的代码块会保留原文并加警告注释。</p><h2 id="v1-2-0-公式实现简说"><a href="#v1-2-0-公式实现简说" class="headerlink" title="v1.2.0 公式实现简说"></a>v1.2.0 公式实现简说</h2><p>纯 Python 后端新增了一条 LaTeX → Word 原生公式的路径：</p><ol><li>扫描 Markdown，把 <code>$...$</code> 和 <code>$$...$$</code> 抠出来，替换成占位符。</li><li>用 <code>latex2mathml</code> 把 LaTeX 转成 MathML。</li><li>用 <code>lxml</code> 遍历 MathML 树，生成 Word 的 <code>m:oMath</code> &#x2F; <code>m:oMathPara</code>。</li><li>行内公式挂到段落里，独立公式包成居中段落。</li><li>转换失败就回退成原始 LaTeX 文本，不会让整个文档崩掉。</li></ol><p>覆盖分数、根号、上下标、极限、n 元运算符、重音、矩阵、括号、围栏等 14 种结构，有 21 个自动化用例校验。</p><p>不过说实话，Pure Python 的 OMML 渲染在分式、根号、大型运算符等复杂式子上还是有缺陷；Pandoc 后端十五年打磨的 TeX 引擎更稳。所以能装 pandoc 的话，复杂公式建议优先 Pandoc 后端。</p><h2 id="两个容易踩的坑"><a href="#两个容易踩的坑" class="headerlink" title="两个容易踩的坑"></a>两个容易踩的坑</h2><p><strong>Windows 乱码</strong></p><p>中文输出在 Windows 下可能乱码，加环境变量：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> PYTHONIOENCODING=utf-8</span><br></pre></td></tr></table></figure><p>PowerShell：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$env:PYTHONIOENCODING</span> = <span class="string">&quot;utf-8&quot;</span></span><br></pre></td></tr></table></figure><p><strong>目录重复</strong></p><p>如果 Markdown 里已经手写了一个”目录”章节，pipeline 又会自动插入 Word 的 TOC 域，结果出现双目录。要么在 Markdown 里删掉手工目录，要么在 <code>project.yaml</code> 里设 <code>styles.toc.enabled: false</code>。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>DOCX Pipeline 让 Markdown → Word 这件事可以一条命令跑完；v1.2.0 之后，公式也不需要 Pandoc 了。写报告、论文、技术文档时，它能省下大量调格式的时间。</p>]]>
    </content>
    <id>https://cyk.pink/posts/23eb6558.html</id>
    <link href="https://cyk.pink/posts/23eb6558.html"/>
    <published>2026-07-22T08:00:00.000Z</published>
    <summary>
      <![CDATA[<p>DOCX Pipeline 是个把 Markdown 转成中文 DOCX 的命令行工具。双后端：Pure Python 零外部依赖，Pandoc 质量更高。v1.2.0 补上了纯 Python 后端对 LaTeX 数学公式的支持，写论文、出报告时不用硬装 Pandoc。</p>
<p>下面先说怎么用，再简单提实现。</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Python 3.10 以上：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip install]]>
    </summary>
    <title>DOCX Pipeline v1.2.0：Markdown 转 Word，顺便把 LaTeX 公式也塞进去</title>
    <updated>2026-07-22T08:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="GPR" scheme="https://cyk.pink/tags/GPR/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <category term="平滑" scheme="https://cyk.pink/tags/%E5%B9%B3%E6%BB%91/"/>
    <content>
      <![CDATA[<p>增益把深部放大后，一些横向窄的条纹、绕射尾、振铃尾也跟着被放大，在 B-scan 上冒成一片斜纹或竖条毛刺。横向平滑就是沿测线方向做均值替换，把这些窄东西糊掉，横向连续的层位则保留。</p><h2 id="和背景去除的区别"><a href="#和背景去除的区别" class="headerlink" title="和背景去除的区别"></a>和背景去除的区别</h2><p>背景去除是「减均值」：删掉横向共有的成分，留下横向局部的点目标。</p><p>横向平滑是「替换为均值」：把每道替换成左右几道的平均，抹掉横向局部的窄条纹，留下横向共有的连续层位。</p><p>两者保留的东西正好相反：</p><table><thead><tr><th></th><th>操作</th><th>去掉</th><th>保留</th></tr></thead><tbody><tr><td>背景去除</td><td>减均值</td><td>横向共有（直达波、水平横条）</td><td>横向局部（钢筋双曲线）</td></tr><tr><td>横向平滑</td><td>替换为均值</td><td>横向局部（绕射尾、条纹）</td><td>横向共有（连续层位）</td></tr></tbody></table><h2 id="单级盒式的问题"><a href="#单级盒式的问题" class="headerlink" title="单级盒式的问题"></a>单级盒式的问题</h2><p>最简单的做法是用一个矩形窗做一级滑动平均。但它的频率响应是 sinc 函数，旁瓣大——阻带里的纹波压不干净，条纹会留残影。</p><h2 id="两级盒式更干净"><a href="#两级盒式更干净" class="headerlink" title="两级盒式更干净"></a>两级盒式更干净</h2><p>做两级串联：第一级用较大窗做主要平滑，第二级用小窗磨掉第一级留下的边缘台阶。两个矩形核卷积后等效成一个梯形核，频响近似 sinc²，旁瓣比单级小得多。</p><p>实测对比（window&#x3D;10）：</p><table><thead><tr><th>归一化频率</th><th>单级响应</th><th>两级响应</th><th>两级压到</th></tr></thead><tbody><tr><td>0.05（通带）</td><td>0.577</td><td>0.558</td><td>97%</td></tr><tr><td>0.15</td><td>0.177</td><td>0.128</td><td>72%</td></tr><tr><td>0.25</td><td>0.091</td><td>0.030</td><td>33%</td></tr><tr><td>0.30</td><td>0.091</td><td>0.012</td><td>13%</td></tr></tbody></table><p>通带内两者差不多，不伤层位；阻带内两级把残余压到单级的 1&#x2F;3~1&#x2F;8。</p><h2 id="梯形核怎么来的"><a href="#梯形核怎么来的" class="headerlink" title="梯形核怎么来的"></a>梯形核怎么来的</h2><p>以 window&#x3D;10 为例：第一级半窗 5（11 点核），第二级半窗 1（3 点核）。算中心点输出时，每个位置被几个第二级抽头「看到」：</p><ul><li>中心位置：3 个抽头都看到，权重最大；</li><li>边缘位置：只有 1 个抽头看到，权重最小。</li></ul><p>把「被看到的次数」画出来就是梯形：1, 2, 3, 3, …, 3, 2, 1。</p><h2 id="窗长怎么选"><a href="#窗长怎么选" class="headerlink" title="窗长怎么选"></a>窗长怎么选</h2><p>只有一个参数 <code>window</code>：平滑跨度，单位是道数。</p><ul><li>斜条纹还在：window 调大；</li><li>横向层位被抹糊了：window 调小，或关掉本步；</li><li>window 的物理含义 ≈ 想抹掉的横向窄目标的宽度，一般 8~20。</li></ul><h2 id="副作用"><a href="#副作用" class="headerlink" title="副作用"></a>副作用</h2><ol><li>会抹掉横向窄的真实目标：小孤腔、细裂隙、单根细钢筋都会被糊掉。</li><li>破坏幅值定量：每个值被换成邻域均值，原始振幅信息没了。</li><li>必须放在流水线最后：它破坏幅值，所有需要真实幅值的步骤都要在它之前。</li></ol><h2 id="边界处理"><a href="#边界处理" class="headerlink" title="边界处理"></a>边界处理</h2><p>图的两端窗会超出范围。我的做法是截断边界，只用地道内真实数据，除以实际参与平均的道数。不补零、不镜像，避免引入假象。</p>]]>
    </content>
    <id>https://cyk.pink/posts/bd00ef30.html</id>
    <link href="https://cyk.pink/posts/bd00ef30.html"/>
    <published>2026-07-22T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>增益把深部放大后，一些横向窄的条纹、绕射尾、振铃尾也跟着被放大，在 B-scan 上冒成一片斜纹或竖条毛刺。横向平滑就是沿测线方向做均值替换，把这些窄东西糊掉，横向连续的层位则保留。</p>
<h2 id="和背景去除的区别"><a href="#和背景去除的区别" class="headerlink"]]>
    </summary>
    <title>横向平滑：抹掉 GPR 剖面上的窄条纹</title>
    <updated>2026-07-22T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="图像处理" scheme="https://cyk.pink/tags/%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86/"/>
    <category term="数据可视化" scheme="https://cyk.pink/tags/%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96/"/>
    <content>
      <![CDATA[<p>处理一批灰度图，深部信号弱、看不清，就加了道增益（gain）把深部的幅值往上提。调完增益打开图一看，奇怪了——本来是想让深部亮起来，结果观感是浅部反而变灰了，深部也没见亮多少。第一反应是增益写错了，查了半天代码没问题。</p><p>后来才反应过来，这不是增益的 bug，是显示的色阶在捣乱。</p><h2 id="全局百分位定色阶，会被放大后的极值撑大"><a href="#全局百分位定色阶，会被放大后的极值撑大" class="headerlink" title="全局百分位定色阶，会被放大后的极值撑大"></a>全局百分位定色阶，会被放大后的极值撑大</h2><p>显示这种动态范围很大的灰度数据，一般不会直接用最小最大值定 vmin&#x2F;vmax——少数极大值（比如直达波那种）会把范围撑得太大，绝大多数数据挤在一头，图就「非黑即白」。常见的做法是用百分位，比如取第 1 和第 99 百分位作为显示的上下限，把长尾削掉，层次就出来了。</p><p>问题在于，这个百分位是<strong>整张图</strong>算的。增益一上去，深部那些本来不强、但被放大了的反射，幅值被抬得很高，整张图的 p99 跟着水涨船高（实测能从不到一万飙到二十多万）。色阶的上限一拉高，那些<strong>没被增益放大的浅部</strong>数据，在新的色阶里就全被挤向中灰了。</p><p>所以调大增益参数，看到的不是「深部变亮」，而是「浅部变灰」——因为色阶被深部的极值撑宽了，浅部在更宽的尺度里自然显得更暗。本质是灰度级被重新分配了，不是数据本身变暗。</p><h2 id="想看增益的真实效果，得把色阶锁住"><a href="#想看增益的真实效果，得把色阶锁住" class="headerlink" title="想看增益的真实效果，得把色阶锁住"></a>想看增益的真实效果，得把色阶锁住</h2><p>要判断增益到底有没有把深部提亮，得让前后两幅图用<strong>同一把尺子</strong>量。具体就是把「加增益前」那张图的百分位 [1,99] 记下来，加增益后显示的时候还用这同一组 vmin&#x2F;vmax，不让它重新算：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">vmin, vmax = np.percentile(raw, [<span class="number">1</span>, <span class="number">99</span>])   <span class="comment"># 用原始图定一次色阶</span></span><br><span class="line"></span><br><span class="line">ax[<span class="number">0</span>].imshow(raw,    vmin=vmin, vmax=vmax, cmap=<span class="string">&#x27;gray&#x27;</span>)   <span class="comment"># 原图</span></span><br><span class="line">ax[<span class="number">1</span>].imshow(gained, vmin=vmin, vmax=vmax, cmap=<span class="string">&#x27;gray&#x27;</span>)   <span class="comment"># 增益后，共用同一色阶</span></span><br></pre></td></tr></table></figure><p>这样色阶锁死了，增益放大的深部目标是真的「冒出来」变亮，浅部因为没动还是原来的灰度，对比一目了然。配套加个「锁定色阶」的开关，勾上两图共用原始百分位，不勾就各自算各自的——平时看绝对层次用各自的，调增益判效果一定得锁。</p><p>这事给我的一个教训：调一个看起来「没生效」的处理参数，先别急着怀疑算法，想想显示环节是不是也跟着动了。增益改的是数据，百分位色阶也跟着数据重新算，两者一叠加，呈现出来的变化就不是单纯增益的效果了。判断一个处理到底有没有用，得把它放在固定的显示基准下看，不然你调的是参数，被糊弄的其实是色阶。</p>]]>
    </content>
    <id>https://cyk.pink/posts/e4b2a96c.html</id>
    <link href="https://cyk.pink/posts/e4b2a96c.html"/>
    <published>2026-07-15T02:30:00.000Z</published>
    <summary>
      <![CDATA[<p>处理一批灰度图，深部信号弱、看不清，就加了道增益（gain）把深部的幅值往上提。调完增益打开图一看，奇怪了——本来是想让深部亮起来，结果观感是浅部反而变灰了，深部也没见亮多少。第一反应是增益写错了，查了半天代码没问题。</p>
<p>后来才反应过来，这不是增益的 bug，是显示的色阶在捣乱。</p>
<h2 id="全局百分位定色阶，会被放大后的极值撑大"><a href="#全局百分位定色阶，会被放大后的极值撑大" class="headerlink" title="全局百分位定色阶，会被放大后的极值撑大"></a>全局百分位定色阶，会被放大后的极值撑大</h2><p>显示这种动态范围很大的灰度数据，一般不会直接用最小最大值定 vmin&#x2F;vmax——少数极大值（比如直达波那种）会把范围撑得太大，绝大多数数据挤在一头，图就「非黑即白」。常见的做法是用百分位，比如取第 1 和第 99]]>
    </summary>
    <title>给图像做完增益别再用整张图的百分位定色阶</title>
    <updated>2026-07-15T02:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="GPR" scheme="https://cyk.pink/tags/GPR/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <category term="增益" scheme="https://cyk.pink/tags/%E5%A2%9E%E7%9B%8A/"/>
    <content>
      <![CDATA[<p>手电筒照得越远越暗，雷达波也一样。深部反射回波比浅部弱很多，不补一下，剖面深处就是一片黑。</p><h2 id="深部为什么衰减"><a href="#深部为什么衰减" class="headerlink" title="深部为什么衰减"></a>深部为什么衰减</h2><p>电磁波往下传，能量衰减来自两方面：</p><ul><li>球面扩散：能量摊开，随距离平方变弱（∝ 1&#x2F;r²）；</li><li>介电吸收：介质吸能，随距离指数衰减（∝ e^(−αz)）。</li></ul><p>深部反射体回波可能比浅部弱 40~60dB。</p><h2 id="时间增益：按时间乘一条固定曲线"><a href="#时间增益：按时间乘一条固定曲线" class="headerlink" title="时间增益：按时间乘一条固定曲线"></a>时间增益：按时间乘一条固定曲线</h2><p>造一条随时间单调增长的曲线 $G(t)$，每道信号逐样点乘上去。浅处乘得小，深处乘得大。</p><p>三种常见形状：</p><p>$$G(t)&#x3D;\begin{cases}1+p,t &amp; \text{linear}\ e^{,p,t} &amp; \text{exp}\ t^{,p}\text{ 归一化} &amp; \text{power}\end{cases}$$</p><p>指数型最贴合指数吸收的物理。设 p&#x3D;0.051，dt&#x3D;0.054ns，512 个样点：</p><table><thead><tr><th>位置</th><th>时间</th><th>增益</th><th>相当于</th></tr></thead><tbody><tr><td>浅</td><td>0 ns</td><td>1.00</td><td>+0 dB</td></tr><tr><td>中</td><td>10 ns</td><td>1.66</td><td>+4.4 dB</td></tr><tr><td>深</td><td>27.6 ns</td><td>4.09</td><td>+12.2 dB</td></tr></tbody></table><p>最深处放大约 4 倍，原本看不见的弱反射被拉到可看的亮度。</p><p>不过别搞错：时间增益不是真正的反衰减，真实吸收系数未知。它只是显示补偿，不是物理还原。</p><h2 id="AGC：自动音量"><a href="#AGC：自动音量" class="headerlink" title="AGC：自动音量"></a>AGC：自动音量</h2><p>AGC 像耳机自动音量：强处压小、弱处放大，让每个深度看起来差不多强。</p><p>对每个样点，取前后小窗口算局部 RMS 能量，然后该样点除以这个 RMS：</p><p>$$\text{rms}[n]&#x3D;\sqrt{\frac{1}{W}\sum_{k&#x3D;-W&#x2F;2}^{W&#x2F;2}x[n+k]^2+\varepsilon},\qquad \text{out}[n]&#x3D;\frac{x[n]}{\text{rms}[n]}$$</p><p>强段除以大数变小，弱段除以小数变大，每小段能量都被归一到约 1。</p><h2 id="两者差别"><a href="#两者差别" class="headerlink" title="两者差别"></a>两者差别</h2><table><thead><tr><th></th><th>时间增益</th><th>AGC</th></tr></thead><tbody><tr><td>曲线</td><td>固定预设</td><td>看数据自适应算</td></tr><tr><td>依据</td><td>时间位置</td><td>邻域实际能量</td></tr><tr><td>效果</td><td>温和补深部</td><td>强行抹平强弱差</td></tr><tr><td>副作用</td><td>改变幅值</td><td>彻底破坏幅值</td></tr></tbody></table><h2 id="gain-后会重新引入直流"><a href="#gain-后会重新引入直流" class="headerlink" title="gain 后会重新引入直流"></a>gain 后会重新引入直流</h2><p>即使 gain 前每道已经是零均值，gain 之后均值又可能不等于 0。增益曲线恒正递增，深部样点被放大更多，会把整道均值往深部拽偏。</p><p>举个例子，dewow 后零均值的道：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">x        = [ 1, -1,  1, -1]     均值 = 0</span><br><span class="line">G        = [ 1,  2,  3,  4]     (浅→深递增)</span><br><span class="line">x·G      = [ 1, -2,  3, -4]     均值 = -0.5</span><br><span class="line">减均值后  = [1.5,-1.5,3.5,-4.5]  均值 = 0</span><br></pre></td></tr></table></figure><p>所以有些流程在 gain 后再减一次每道均值，称为 <code>post_gain_dc</code>。它跟 dewow 不是一回事：dewow 是去低频漂移，<code>post_gain_dc</code> 是清 gain 新引入的直流。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li>增益&#x2F;AGC 会放大噪声，调太大深部就全是雪花。</li><li>改变幅值关系，不能定量。「反射越强 &#x3D; 病害越重」必须回原始数据。</li><li>AGC 比固定增益更激进，仅用于显示和目标识别，绝不能用于定量。</li></ul><p>AGC 窗长一般取目标回波持续长度的 2~3 倍；固定增益的 p 调到深部看得见、但噪声还没明显冒出来就行。</p>]]>
    </content>
    <id>https://cyk.pink/posts/5c056d5b.html</id>
    <link href="https://cyk.pink/posts/5c056d5b.html"/>
    <published>2026-07-15T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>手电筒照得越远越暗，雷达波也一样。深部反射回波比浅部弱很多，不补一下，剖面深处就是一片黑。</p>
<h2 id="深部为什么衰减"><a href="#深部为什么衰减" class="headerlink" title="深部为什么衰减"></a>深部为什么衰减</h2><p>电磁波往下传，能量衰减来自两方面：</p>
<ul>
<li>球面扩散：能量摊开，随距离平方变弱（∝ 1&#x2F;r²）；</li>
<li>介电吸收：介质吸能，随距离指数衰减（∝ e^(−αz)）。</li>
</ul>
<p>深部反射体回波可能比浅部弱 40~60dB。</p>
<h2 id="时间增益：按时间乘一条固定曲线"><a href="#时间增益：按时间乘一条固定曲线" class="headerlink" title="时间增益：按时间乘一条固定曲线"></a>时间增益：按时间乘一条固定曲线</h2><p>造一条随时间单调增长的曲线]]>
    </summary>
    <title>GPR 时间增益与 AGC：把深部提亮的两条路</title>
    <updated>2026-07-15T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="二进制" scheme="https://cyk.pink/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6/"/>
    <category term="数据处理" scheme="https://cyk.pink/tags/%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <content>
      <![CDATA[<p>把一批 16 位采样的数据读进来做了点预处理（去直流、去背景这种减法操作），再写回原来的二进制文件给别的专业软件打开。读进来处理的时候一切正常，写回去打开一看，有的文件好好的，有的直接满屏噪点、糊成一团。同样的代码、同样的处理，结果天差地别，卡了好一阵。</p><p>根子是 16 位整数到底按有符号还是无符号解读，不是统一的，取决于文件格式。</p><h2 id="同样-16-位，有的围绕-0-有的围绕-32768"><a href="#同样-16-位，有的围绕-0-有的围绕-32768" class="headerlink" title="同样 16 位，有的围绕 0 有的围绕 32768"></a>同样 16 位，有的围绕 0 有的围绕 32768</h2><p>这批数据两种格式：一种是无符号 16 位（unsigned），数据的载波零点在 32768，数值在 0<del>65535 之间抖；另一种是有符号 16 位（signed），围绕 0 抖，范围是 −32768</del>32767。同样是两个字节 <code>00 80</code>，按无符号读是 32768，按有符号读（小端）是 −32768，完全是两码事。</p><p>读的时候按各自格式来，没问题。问题出在写回。</p><h2 id="减法预处理之后有了负值"><a href="#减法预处理之后有了负值" class="headerlink" title="减法预处理之后有了负值"></a>减法预处理之后有了负值</h2><p>去直流、去背景这类操作本质是减法：估一个背景再从原数据里减掉。减完之后，数据就从「围绕 32768」或者「围绕 0」变成了「围绕 0」，而且必然出现负值（原来低于背景的地方）。</p><p>写回有符号格式（围绕 0）的那种文件，很直接——数据本来就围绕 0，减完还是围绕 0，按有符号 clamp 到 [−32768, 32767] 写进去就行，啥额外操作都不用加。</p><p>但写回无符号格式（围绕 32768）的那种文件就出事了：减法之后围绕 0、带负值，直接当无符号写回去，那些负值会被解释成 65536 附近的超大数，落在一个本该是中灰的位置上全是亮点，整张图就满屏噪点了。</p><h2 id="写回无符号得先平移回它的基准"><a href="#写回无符号得先平移回它的基准" class="headerlink" title="写回无符号得先平移回它的基准"></a>写回无符号得先平移回它的基准</h2><p>所以写回无符号格式之前，得先把数据从「围绕 0」平移回「围绕 32768」——加一个全图统一的直流基准（大概就是整图的均值，约等于 32768），把负值整体抬进 0~65535 的范围，再 clamp：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> target_unsigned:</span><br><span class="line">    dc_base = <span class="built_in">int</span>(<span class="built_in">round</span>(data.mean()))     <span class="comment"># 全图统一的基准，约 32768</span></span><br><span class="line">    out = data + dc_base                   <span class="comment"># 平移回无符号范围</span></span><br><span class="line">    out = np.clip(out, <span class="number">0</span>, <span class="number">65535</span>).astype(<span class="string">&#x27;&lt;u2&#x27;</span>)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">    out = np.clip(data, -<span class="number">32768</span>, <span class="number">32767</span>).astype(<span class="string">&#x27;&lt;i2&#x27;</span>)   <span class="comment"># 有符号直接 clamp，不加基准</span></span><br></pre></td></tr></table></figure><p>有个反着的坑得强调一下：写回有符号格式<strong>绝对不能</strong>也傻乎乎加个 +32768 的基准。因为数据本来围绕 0，加完 32768 之后落到 32768 附近，再被 <code>(short)</code> 强转，超过 32767 的部分直接溢回 −32768，数据当场写坏。也就是说 clamp 方式必须按目标格式二选一，不能图省事统一一套。</p><p>还有一个细节：那个直流基准必须是<strong>全图统一</strong>的一个值，不能用每一道各自的均值。用各道均值的话，等于把每道都单独拉回自己的 32768，结果是保留了各道之间的直流差——这恰恰是去直流、去背景想抹掉的东西，效果会差很远。这一条是走过弯路之后才纠正过来的：一开始图精确用了各道均值，结果处理完的图还不如不平移干净，换成全图统一直流基准才对。</p><p>说到底，这事的核心就一句：16 位数据的有符号、无符号不是读的时候定死就完了，写回的时候还得按目标格式再判一次。同样是「减完带负值」的中间结果，写回有符号和无符号要走的 clamp 路径完全不同，搞混了就是满屏噪点和数据写坏的区别。</p>]]>
    </content>
    <id>https://cyk.pink/posts/d6c7a14b.html</id>
    <link href="https://cyk.pink/posts/d6c7a14b.html"/>
    <published>2026-07-14T05:25:00.000Z</published>
    <summary>
      <![CDATA[<p>把一批 16 位采样的数据读进来做了点预处理（去直流、去背景这种减法操作），再写回原来的二进制文件给别的专业软件打开。读进来处理的时候一切正常，写回去打开一看，有的文件好好的，有的直接满屏噪点、糊成一团。同样的代码、同样的处理，结果天差地别，卡了好一阵。</p>
<p>根子是 16 位整数到底按有符号还是无符号解读，不是统一的，取决于文件格式。</p>
<h2 id="同样-16-位，有的围绕-0-有的围绕-32768"><a href="#同样-16-位，有的围绕-0-有的围绕-32768" class="headerlink" title="同样 16 位，有的围绕 0 有的围绕 32768"></a>同样 16 位，有的围绕 0 有的围绕 32768</h2><p>这批数据两种格式：一种是无符号 16 位（unsigned），数据的载波零点在 32768，数值在 0<del>65535 之间抖；另一种是有符号 16 位（signed），围绕 0 抖，范围是 −32768</del>32767。同样是两个字节 <code>00 80</code>，按无符号读是]]>
    </summary>
    <title>16 位整数写回文件有符号和无符号要分开 clamp</title>
    <updated>2026-07-14T05:25:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="二进制" scheme="https://cyk.pink/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="Python" scheme="https://cyk.pink/tags/Python/"/>
    <category term="逆向" scheme="https://cyk.pink/tags/%E9%80%86%E5%90%91/"/>
    <content>
      <![CDATA[<p>手上有一批设备采集下来的二进制数据，没文档，只有几组不同后缀的文件，要把它们读出来画图。全程靠 hex 编辑器一点点猜，记一下逆向这种私有二进制格式的大概路数，顺便记一个踩得很疼的 off-by-one。</p><h2 id="先把头和样点数摸清楚"><a href="#先把头和样点数摸清楚" class="headerlink" title="先把头和样点数摸清楚"></a>先把头和样点数摸清楚</h2><p>这种采集文件，基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器，前面一段往往是固定模式、跟后面的「数据区」长得不一样，头一般是个整数（512、1024、2048 这种）。这批文件摸下来头是 1024 字节。</p><p>样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法：<code>(文件总大小 - 头) / 每道字节数 = 道数</code>，而 <code>每道字节数 = 样点数 × 每个样点的字节</code>。如果数据是 16 位（2 字节）存的，那去文件头里找一个值，使得「头之后的字节数 &#x2F; 2 &#x2F; 这个值」是个整数、而且这个值本身是合理的样点数（比如 512）。这批文件样点数在头偏移 4 的位置，值是 512。</p><p>到这里，最朴素的读法就有了：跳过 1024 头，后面按 int16 一道一道读：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">nsamp = <span class="number">512</span>           <span class="comment"># 头里偏移4读出来的样点数</span></span><br><span class="line">header = <span class="number">1024</span>         <span class="comment"># 头大小</span></span><br><span class="line">raw = np.frombuffer(data[header:], dtype=<span class="string">&#x27;&lt;i2&#x27;</span>)   <span class="comment"># 小端 int16</span></span><br><span class="line">ntrace = raw.size // nsamp</span><br><span class="line">traces = raw[:ntrace * nsamp].reshape(ntrace, nsamp)</span><br></pre></td></tr></table></figure><h2 id="用-magic-字节区分格式"><a href="#用-magic-字节区分格式" class="headerlink" title="用 magic 字节区分格式"></a>用 magic 字节区分格式</h2><p>事情没这么简单，因为这批数据有四种后缀、对应四种格式，得知道手里这份是哪种。靠后缀判断不可靠（后缀能随便改），靠谱的是文件头里的「魔数」——每个格式在固定位置有一段独特的字节，相当于这个格式的身份证。</p><p>实测下来这批文件：一种开头是 <code>00 07</code>，一种是 <code>56 04 00 00 04 04 00 00</code>，另两种是 <code>01 00</code> 或 <code>00 00</code>。读的时候先嗅探（sniff）一下这几个字节，命中哪种就走哪种的解析器，后缀只当兜底：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">read_auto</span>(<span class="params">path</span>):</span><br><span class="line">    head = <span class="built_in">open</span>(path, <span class="string">&#x27;rb&#x27;</span>).read(<span class="number">8</span>)</span><br><span class="line">    <span class="keyword">if</span> head[:<span class="number">2</span>] == <span class="string">b&#x27;\x00\x07&#x27;</span>:          <span class="keyword">return</span> read_kind_a(path)</span><br><span class="line">    <span class="keyword">if</span> head[:<span class="number">4</span>] == <span class="string">b&#x27;\x56\x04\x00\x00&#x27;</span>:  <span class="keyword">return</span> read_kind_b(path)</span><br><span class="line">    <span class="comment"># 剩下的退回看后缀</span></span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>有意思的是，其中三种格式摸到最后发现是同一个套路——都是 <code>[1024 头][int16 数据]</code>，只是 magic 不一样，连样点数偏移都一样（纯属不同厂家巧合撞了布局）。所以这三个的解析逻辑抽到了一个公共函数，各自只是薄薄包一层填自己的 magic。</p><h2 id="一个-off-by-one，数据道数多算一条"><a href="#一个-off-by-one，数据道数多算一条" class="headerlink" title="一个 off-by-one，数据道数多算一条"></a>一个 off-by-one，数据道数多算一条</h2><p>真正折腾人的是第四种。一开始按老套路把它当「<code>[1024 头][int16]</code>」来读，画出来的图第一道总是一根奇怪的直线、后面整体对不齐，怎么调参数都不对。</p><p>后来把它的字节流摊开一格一格看，才发现它根本不是「全局头 + 数据」那种结构，而是<strong>定长记录流</strong>：整个文件就是一条条固定 1028 字节的记录首尾拼接，没有独立的全局头。而且记录还分类型：</p><ul><li>第 0 条是「头记录」，前 4 字节是类型码 <code>1110</code>，后面基本是 0；</li><li>后面才是数据道记录，每条开头是类型码 <code>82</code>，再跟 512 个 int16 样点。</li></ul><p>之前的读法把第 0 条头记录也当成了数据道，所以道数多算了一条、第一道里混进了那条头记录的残值，图自然对不齐。改完之后，先检测开头是不是 <code>1110</code>、是的话跳过这条头记录，真实道数从第 1 条开始数，图一下就正了。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">read_record_stream</span>(<span class="params">data</span>):</span><br><span class="line">    rec = <span class="number">1028</span></span><br><span class="line">    nrec = <span class="built_in">len</span>(data) // rec</span><br><span class="line">    first = np.frombuffer(data[:<span class="number">4</span>], dtype=<span class="string">&#x27;&lt;i4&#x27;</span>)[<span class="number">0</span>]</span><br><span class="line">    start = <span class="number">1</span> <span class="keyword">if</span> first == <span class="number">1110</span> <span class="keyword">else</span> <span class="number">0</span>      <span class="comment"># 头记录(type=1110)不算数据道</span></span><br><span class="line">    traces = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(start, nrec):</span><br><span class="line">        seg = data[i*rec + <span class="number">4</span> : i*rec + <span class="number">4</span> + <span class="number">512</span>*<span class="number">2</span>]   <span class="comment"># 跳过 type 码读 512 样点</span></span><br><span class="line">        traces.append(np.frombuffer(seg, dtype=<span class="string">&#x27;&lt;i2&#x27;</span>))</span><br><span class="line">    <span class="keyword">return</span> np.array(traces)</span><br></pre></td></tr></table></figure><p>逆向这种没文档的二进制格式，路数其实挺固定：先估头大小、再从文件大小反推样点数偏移、用 magic 区分同源的几种格式、剩下就是格格里看字节把记录结构猜出来。最后那个 off-by-one 也提醒一件事——别想当然觉得同一批数据都是一种结构，哪怕是同一个采集软件出的，不同后缀可能用的是完全不同的文件模型，按老套路硬套就会多算一条少算一条。</p>]]>
    </content>
    <id>https://cyk.pink/posts/8b2c5e91.html</id>
    <link href="https://cyk.pink/posts/8b2c5e91.html"/>
    <published>2026-07-13T01:40:00.000Z</published>
    <summary>
      <![CDATA[<p>手上有一批设备采集下来的二进制数据，没文档，只有几组不同后缀的文件，要把它们读出来画图。全程靠 hex 编辑器一点点猜，记一下逆向这种私有二进制格式的大概路数，顺便记一个踩得很疼的 off-by-one。</p>
<h2 id="先把头和样点数摸清楚"><a href="#先把头和样点数摸清楚" class="headerlink" title="先把头和样点数摸清楚"></a>先把头和样点数摸清楚</h2><p>这种采集文件，基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器，前面一段往往是固定模式、跟后面的「数据区」长得不一样，头一般是个整数（512、1024、2048 这种）。这批文件摸下来头是 1024 字节。</p>
<p>样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法：<code>(文件总大小 - 头) / 每道字节数 = 道数</code>，而 <code>每道字节数 = 样点数 × 每个样点的字节</code>。如果数据是 16 位（2 字节）存的，那去文件头里找一个值，使得「头之后的字节数]]>
    </summary>
    <title>逆向一种私有二进制格式从文件头和 magic 字节说起</title>
    <updated>2026-07-13T01:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<p>雷达数据做完前面的清洗，最后一步往往是加增益（gain）。深部反射看不见、一团黑，加完增益深部就亮起来了。但增益到底在补什么、补多少有讲究，不是无脑把深部拉亮就行。另外有篇单独讲做完增益之后显示色阶的坑，这篇只讲增益算法本身。</p><h2 id="为什么要补：深部比浅部弱太多"><a href="#为什么要补：深部比浅部弱太多" class="headerlink" title="为什么要补：深部比浅部弱太多"></a>为什么要补：深部比浅部弱太多</h2><p>电磁波往地下走，越深衰减越厉害，所以深处的反射回到天线时已经弱了一大截。一道信号画出来，浅部反射很强、深部几乎贴着噪声底，深部想看的目标（比如衬砌后面的脱空）就淹没在里面。增益的作用就是把这个随深度掉下去的幅度，人为往上提一提，让深浅在图上都能看见。</p><h2 id="衰减有两种，数学形式不一样"><a href="#衰减有两种，数学形式不一样" class="headerlink" title="衰减有两种，数学形式不一样"></a>衰减有两种，数学形式不一样</h2><p>深部变弱这件事，根上有两个来源，得分开看。一个是几何扩散——波往外是球面扩散的，能量摊到越来越大的球面上，幅度按幂律（大约 1&#x2F;距离）往下掉。另一个是介电吸收——介质本身把电磁能吃掉一部分，这个是按指数 e^(−α·深度) 掉的。</p><p>关键在于这俩数学形式不同：一个是幂律、一个是指数。所以想补偿，也得用对应的形式去补，硬拿一个套另一个补不齐。</p><h2 id="两种增益对应两种衰减"><a href="#两种增益对应两种衰减" class="headerlink" title="两种增益对应两种衰减"></a>两种增益对应两种衰减</h2><p>增益常见的两种类型正好对应上：power 型按幂律补，对应几何扩散；exp 型按指数补，对应介电吸收。要补哪种衰减就选哪种类型。实际数据里两种衰减同时存在，单用 exp 其实只补了吸收那部分，几何扩散那块没单独补——这正是后面会看到的「补不满」的根因。</p><h2 id="exp-的参数-p-不是拍脑袋"><a href="#exp-的参数-p-不是拍脑袋" class="headerlink" title="exp 的参数 p 不是拍脑袋"></a>exp 的参数 p 不是拍脑袋</h2><p>exp 增益的形式是逐点乘一条指数曲线，关键参数是 p。这里有个容易踩的坑：p 的单位是 1&#x2F;ns，物理上等于 α·v——α 是介质的吸收系数（每米吸收多少），v 是波速（每纳秒走多少米）。所以 p 不是随便给的数，是能从数据反推的：算一下能量随时间的衰减斜率（对数能量随时间的斜率），那基本就是 α·v，也就是该用的 p。实测下来一批数据这个斜率大概在 -0.045&#x2F;ns 附近，用 0.05&#x2F;ns 左右就接近数据本身反推出来的值。</p><h2 id="故意不补满"><a href="#故意不补满" class="headerlink" title="故意不补满"></a>故意不补满</h2><p>这里有个反直觉的点：实测深部相对浅部衰减了大约 18.5dB，但用 exp(0.05) 只补回来约 12dB，还留着约 6dB 没补。这不算错，是故意的。补得太狠，深浅都被拉平，深部强反射和浅部一样亮，反而看不出深浅层次了。判厚度、判脱空这些是要靠相对强弱判断的，全拉平了反倒没法判。所以温和地补、留一点层次，是刻意的。真要完全补平，p 得往 0.077 那边走。</p><p>换句话说，增益是个显示补偿，不是物理反演。它把每道逐点乘同一条 G(t) 曲线（所有道乘同一条），目的是让人眼看得见深部，但不会把衰减掉的能量真的还原回来。增益后的幅值不能拿来当真实幅值定量算。</p><h2 id="保幅就别用-AGC"><a href="#保幅就别用-AGC" class="headerlink" title="保幅就别用 AGC"></a>保幅就别用 AGC</h2><p>还有个 AGC（自动增益控制），它不分深浅物理机制，直接把局部能量拉平归一——原来局部能量跨深度抖得很厉害（标准差好几千），AGC 一压，各处都归到差不多 1，标准差能压到零点零几。看图特别爽，深浅都清楚。但代价是幅值信息全没了：哪里本来强、哪里本来弱，AGC 之后全都拉平，相对强弱看不出来了。</p><p>所以选 exp 还是 AGC，看目的。要保幅、要做定量判读（靠幅值判厚度、判脱空含水），就用 exp，别用 AGC。AGC 只适合纯粹看个图、不在乎幅值大小的场景。</p><h2 id="切类型要重定参数"><a href="#切类型要重定参数" class="headerlink" title="切类型要重定参数"></a>切类型要重定参数</h2><p>最后说个具体的坑：exp 的 p 单位是 1&#x2F;ns，power 的 p 是个无量纲的幂指数，这俩「p」名字一样但含义完全不同。从 exp 切到 power（或反过来），参数不能直接照搬，得按新类型的含义重新定，不然补出来的曲线完全不是想要的那条。</p><p>增益这一步看着简单（就是逐点乘一条曲线），真正要拿捏的是补什么、补多少。补的是随深度掉的幅度，形式得匹配衰减机制（幂律对几何、指数对吸收），力度上故意留一点层次别拉平，保幅用 exp、纯看图才用 AGC。把它当显示补偿、不当物理反演，就不会指望它把深部能量真的还原回来。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9c4e8a2f.html</id>
    <link href="https://cyk.pink/posts/9c4e8a2f.html"/>
    <published>2026-07-12T01:30:00.000Z</published>
    <summary>
      <![CDATA[<p>雷达数据做完前面的清洗，最后一步往往是加增益（gain）。深部反射看不见、一团黑，加完增益深部就亮起来了。但增益到底在补什么、补多少有讲究，不是无脑把深部拉亮就行。另外有篇单独讲做完增益之后显示色阶的坑，这篇只讲增益算法本身。</p>
<h2 id="为什么要补：深部比浅部弱太多"><a href="#为什么要补：深部比浅部弱太多" class="headerlink" title="为什么要补：深部比浅部弱太多"></a>为什么要补：深部比浅部弱太多</h2><p>电磁波往地下走，越深衰减越厉害，所以深处的反射回到天线时已经弱了一大截。一道信号画出来，浅部反射很强、深部几乎贴着噪声底，深部想看的目标（比如衬砌后面的脱空）就淹没在里面。增益的作用就是把这个随深度掉下去的幅度，人为往上提一提，让深浅在图上都能看见。</p>
<h2 id="衰减有两种，数学形式不一样"><a href="#衰减有两种，数学形式不一样" class="headerlink"]]>
    </summary>
    <title>雷达数据做增益到底在补什么</title>
    <updated>2026-07-12T01:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Java" scheme="https://cyk.pink/tags/Java/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="防破解" scheme="https://cyk.pink/tags/%E9%98%B2%E7%A0%B4%E8%A7%A3/"/>
    <content>
      <![CDATA[<p>给桌面软件接了一套登录许可证，启动的时候要过一串校验，任一条不过就直接锁应用。licence 防破解的一些具体坑（时间回拨、指纹漂移）之前写过，这里不重复，主要想理一下：一张许可证启动时到底该查哪些项，以及其中几个不那么直观的、容易漏掉的检查。</p><p>整体是个串行链条，前面是许可证本身合不合法，后面是运行环境对不对，最后才是用户名密码。哪一环断了都锁，不是「前面过了后面就随便」。</p><h2 id="许可证本体这几项"><a href="#许可证本体这几项" class="headerlink" title="许可证本体这几项"></a>许可证本体这几项</h2><p>先是把许可证解析出来，格式不对、签名解析失败，直接锁。然后是有效期，过期了锁——但有效期的判断会被改系统时间绕掉，这块得配合本地单调时间记录来防回拨，之前那篇说过的思路，这里不展开。接着是硬件指纹比对，确认现在这台机器就是许可证绑的那台，防一份 licence 拷来拷去到处用。</p><p>这几项是基础，大多数做 licence 的人都会想到。</p><h2 id="版本号得跟软件版本对上"><a href="#版本号得跟软件版本对上" class="headerlink" title="版本号得跟软件版本对上"></a>版本号得跟软件版本对上</h2><p>这条我觉得容易被忽略。许可证里带一个版本号字段，校验的时候不能只看它「有没有」，得看它是不是等于当前软件的版本。</p><p>为什么重要——因为软件会迭代。一份在 v2 时代签发的 licence，里面版本号写的是 2，用户拿着它去跑 v3，如果不校验版本，v3 照样认这份老 licence。这就留下一个口子：买了旧版的人拿着旧 licence 一直白嫖新版本。绑死版本号之后，大版本升级要么重新签发、要么 licence 里显式声明向前兼容哪些版本，主动权在签发方手里。</p><h2 id="注册目录得是当初注册的那个"><a href="#注册目录得是当初注册的那个" class="headerlink" title="注册目录得是当初注册的那个"></a>注册目录得是当初注册的那个</h2><p>这条更隐蔽。注册成功的时候，把「当时所在的安装目录」记进许可证或者本地凭证里。下次启动，拿当前程序的运行目录（<code>user.dir</code>）跟记下来的那个比一下，不一致就锁。</p><p>防的是这种操作：用户在一台机器上注册成功了，然后把整个安装目录连着注册凭证一起打包，拷到另一台机器上跑。光靠硬件指纹的话，如果两台机器硬件凑巧接近、或者指纹本身不够强，可能蒙混过去。但目录是物理路径，拷到别的机器上基本不可能还在同一个绝对路径下，这条等于多加了一层「这确实是当初那台机器、那个位置」的绑定。</p><h2 id="配置校核防的是改配置"><a href="#配置校核防的是改配置" class="headerlink" title="配置校核防的是改配置"></a>配置校核防的是改配置</h2><p>还有个配置校核（proofread）：启动时把关键的本地配置重新算一遍校验和，跟之前存的比对。防的是破解者不去动 licence 本体（那个有签名，动不了），转而去改本地的配置文件——比如把「已激活」标志、剩余时长这些写死成有利值。配置一旦被改，校验和对不上，锁。</p><p>这条的逻辑是：licence 本体有签名护着改不动，那攻击者最可能下手的就是没有签名保护的本地配置，得给这部分也上个完整性校验。</p><h2 id="最后才是用户名密码"><a href="#最后才是用户名密码" class="headerlink" title="最后才是用户名密码"></a>最后才是用户名密码</h2><p>所有上面的环境校验都过了，才走到用户名密码这一步。它放在最后是有讲究的：用户名密码防的是「会操作电脑但不会逆向」的人，真正能破前面那些链路的人，根本不需要猜密码。所以把它放最前面没意义，先确认环境和许可证没问题，最后再用密码挡一道普通使用者层面的越权。</p><p>这一串串起来，思路其实就是「licence 本体靠签名、运行环境靠多维度绑定」。签名保证许可证没被改，版本号、目录、配置校核把「拿合法 licence 换个环境用」这条路一层层堵上，最后用密码挡住普通越权。哪条单独拎出来都不绝对，但叠成一个链条之后，想绕过去得同时把好几处都对上，门槛就上去了。</p>]]>
    </content>
    <id>https://cyk.pink/posts/3f7d9c2b.html</id>
    <link href="https://cyk.pink/posts/3f7d9c2b.html"/>
    <published>2026-07-11T07:55:00.000Z</published>
    <summary>
      <![CDATA[<p>给桌面软件接了一套登录许可证，启动的时候要过一串校验，任一条不过就直接锁应用。licence 防破解的一些具体坑（时间回拨、指纹漂移）之前写过，这里不重复，主要想理一下：一张许可证启动时到底该查哪些项，以及其中几个不那么直观的、容易漏掉的检查。</p>
<p>整体是个串行链条，前面是许可证本身合不合法，后面是运行环境对不对，最后才是用户名密码。哪一环断了都锁，不是「前面过了后面就随便」。</p>
<h2 id="许可证本体这几项"><a href="#许可证本体这几项" class="headerlink" title="许可证本体这几项"></a>许可证本体这几项</h2><p>先是把许可证解析出来，格式不对、签名解析失败，直接锁。然后是有效期，过期了锁——但有效期的判断会被改系统时间绕掉，这块得配合本地单调时间记录来防回拨，之前那篇说过的思路，这里不展开。接着是硬件指纹比对，确认现在这台机器就是许可证绑的那台，防一份 licence 拷来拷去到处用。</p>
<p>这几项是基础，大多数做 licence 的人都会想到。</p>
<h2 id="版本号得跟软件版本对上"><a]]>
    </summary>
    <title>一张登录许可证启动时到底该查哪些</title>
    <updated>2026-07-11T07:55:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Java" scheme="https://cyk.pink/tags/Java/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="防破解" scheme="https://cyk.pink/tags/%E9%98%B2%E7%A0%B4%E8%A7%A3/"/>
    <content>
      <![CDATA[<p>做了个注册机给桌面软件签发许可证，注册机本身有个管理员解锁密码——不是最终用户的密码，是防止谁都能进来乱签 license 的那道门。一开始图省事，密码明文写在 <code>gradle.properties</code> 里，程序运行的时候读出来比对。能用是能用，但越想越不对劲。</p><p>问题在于，这个密码随包一起发出去了。<code>gradle.properties</code> 里的值，打包完之后会以某种形式落到产物里，最差就是直接生成一个 <code>register.properties</code> 跟着 exe 走。破解者拿到发版包，翻一翻配置文件，明文密码就在那儿躺着，这道门等于没上锁。</p><p>后来改成这样：密码在打包阶段就转成 MD5，然后通过 exe4j 的 VM 参数固化进 exe，运行时直接从系统属性里读 MD5 来比对，全程不落外部配置文件。</p><p>打包脚本里大致是：</p><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 打包时把明文密码算成 MD5，塞进 exe4j 的 VM 参数</span></span><br><span class="line"><span class="keyword">def</span> pwdMd5 = md5(project.property(<span class="string">&#x27;registerPassword&#x27;</span>))</span><br><span class="line"><span class="keyword">def</span> vmArgs = <span class="string">&quot;-Dregister.password.md5=$&#123;pwdMd5&#125;&quot;</span></span><br><span class="line"><span class="comment">// 这段 vmArgs 写进 exe4j 配置，随 exe 一起固化</span></span><br></pre></td></tr></table></figure><p>运行时校验这边，按优先级找这个 MD5：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> String <span class="title function_">resolveExpectedPasswordMd5</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 打包时通过 -Dregister.password.md5 注入的（生产）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">md5</span> <span class="operator">=</span> System.getProperty(<span class="string">&quot;register.password.md5&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (StringUtils.isNotEmpty(md5)) &#123;</span><br><span class="line">        <span class="keyword">return</span> md5;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2. 开发态 IDEA 直接跑，读 properties 里的明文现算（仅本机）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">pwd</span> <span class="operator">=</span> loadDevProperties().getProperty(<span class="string">&quot;registerPassword&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (StringUtils.isNotEmpty(pwd)) &#123;</span><br><span class="line">        <span class="keyword">return</span> md5(pwd);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3. 兜底</span></span><br><span class="line">    <span class="keyword">return</span> md5(FALLBACK_PASSWORD);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用户输入密码，同样 <code>md5</code> 一遍，跟期望值比，对得上才放行。全程只存和比 MD5，明文密码不进运行态、不落盘。</p><p>这么改之后几个好处。一是发版包里翻不到明文密码了，最起码「翻配置文件」这条路堵死了，想拿原值得反 exe。二是 VM 参数 <code>-D</code> 是打进 exe4j 配置、随 exe 固化的，不是旁边带个文件，没那么容易被一眼发现和替换。三是开发态不受影响——IDEA 里直接跑 main 的时候没有那个 VM 参数，就走第二条读 properties 现算，本机调试照样方便。</p><p>当然这不是什么高强度的防护，MD5 本身早不该直接用来存密码（正经得加盐、用慢哈希），注册机这个场景门槛要求没那么高，这么搞够用。真要紧的是别把明文密码跟包一起发出去——那不叫加密，那叫附赠钥匙。</p>]]>
    </content>
    <id>https://cyk.pink/posts/c4e2b81a.html</id>
    <link href="https://cyk.pink/posts/c4e2b81a.html"/>
    <published>2026-07-10T03:08:00.000Z</published>
    <summary>
      <![CDATA[<p>做了个注册机给桌面软件签发许可证，注册机本身有个管理员解锁密码——不是最终用户的密码，是防止谁都能进来乱签 license 的那道门。一开始图省事，密码明文写在 <code>gradle.properties</code> 里，程序运行的时候读出来比对。能用是能用，但越想越不对劲。</p>
<p>问题在于，这个密码随包一起发出去了。<code>gradle.properties</code> 里的值，打包完之后会以某种形式落到产物里，最差就是直接生成一个 <code>register.properties</code> 跟着 exe 走。破解者拿到发版包，翻一翻配置文件，明文密码就在那儿躺着，这道门等于没上锁。</p>
<p>后来改成这样：密码在打包阶段就转成 MD5，然后通过 exe4j 的 VM 参数固化进 exe，运行时直接从系统属性里读 MD5 来比对，全程不落外部配置文件。</p>
<p>打包脚本里大致是：</p>
<figure class="highlight groovy"><table><tr><td class="code"><pre><span]]>
    </summary>
    <title>注册机的管理员密码别明文写在配置文件里</title>
    <updated>2026-07-10T03:08:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="JavaFX" scheme="https://cyk.pink/tags/JavaFX/"/>
    <category term="打包" scheme="https://cyk.pink/tags/%E6%89%93%E5%8C%85/"/>
    <content>
      <![CDATA[<p>用 exe4j 把一个 JavaFX 桌面程序打成 exe，自带 JRE 一起发出去，结果双击 exe 直接弹「找不到 jvm」之类的错。机器上明明装了 Java，自带的 JRE 目录也都在，就是起不来。</p><p>折腾一圈发现是 exe4j 找 JRE 的路径没配对。</p><p>exe4j 打出来的 exe 本质是个启动器，它自己不包含 JVM，运行的时候要先在机器上找一个可用的 JRE 再拉起真正的程序。它找 JRE 是按一个「搜索顺序」来的，默认会先去找系统注册的 Java、<code>JAVA_HOME</code> 之类，找不到才往下走。问题就出在这：开发机上系统 Java 是高版本的，能蒙混过去；到了用户那台干净机器上，啥系统 Java 都没有，它又没被告诉「去我自带的那个 .&#x2F;jre 里找」，自然就报找不到。</p><p>修法是在 exe4j 的配置里显式给一个搜索路径，优先指向打包进去的那个 JRE 目录。exe4j 的配置是份 xml，对应的是 <code>&lt;searchSequence&gt;</code> 这一段：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">searchSequence</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">directory</span> <span class="attr">location</span>=<span class="string">&quot;./jre&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">searchSequence</span>&gt;</span></span><br></pre></td></tr></table></figure><p>就是把「相对于 exe 所在目录的 <code>./jre</code>」放到搜索序列的最前面。打包的时候把精简过的 JRE（我用 jlink 裁了一份只含要用的模块的）整个放到发版目录的 <code>jre/</code> 下，exe 起来就先从这儿找，稳稳命中自带的那份，跟用户机器上装没装 Java 彻底脱钩。</p><p>有个坑顺带说一下：这个 JRE 目录名得和配置里写的对得上，打包脚本挪文件的时候别把 <code>jre</code> 改名或者放到别的层级，否则 <code>./jre</code> 这个相对路径就指空了，又是同样找不到的报错。我用 Gradle task 自动生成这份 exe4j 配置 + 拷 JRE，两步用同一个目录变量串起来，省得手抖对不上。</p><p>其实这类「在我这好好的、到你那不行」的问题，根子基本都是把开发机上的环境当成了默认环境。启动器找 JRE 这种事，别指望它自己去猜，显式把自带 JRE 的路径塞到搜索序列最前面，一份自包含的 exe 才算真的能到处跑。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9f3a7c2e.html</id>
    <link href="https://cyk.pink/posts/9f3a7c2e.html"/>
    <published>2026-07-09T08:20:00.000Z</published>
    <summary>
      <![CDATA[<p>用 exe4j 把一个 JavaFX 桌面程序打成 exe，自带 JRE 一起发出去，结果双击 exe 直接弹「找不到 jvm」之类的错。机器上明明装了 Java，自带的 JRE 目录也都在，就是起不来。</p>
<p>折腾一圈发现是 exe4j 找 JRE 的路径没配对。</p>
<p>exe4j 打出来的 exe 本质是个启动器，它自己不包含 JVM，运行的时候要先在机器上找一个可用的 JRE 再拉起真正的程序。它找 JRE 是按一个「搜索顺序」来的，默认会先去找系统注册的 Java、<code>JAVA_HOME</code> 之类，找不到才往下走。问题就出在这：开发机上系统 Java 是高版本的，能蒙混过去；到了用户那台干净机器上，啥系统 Java 都没有，它又没被告诉「去我自带的那个 .&#x2F;jre 里找」，自然就报找不到。</p>
<p>修法是在 exe4j 的配置里显式给一个搜索路径，优先指向打包进去的那个 JRE 目录。exe4j 的配置是份 xml，对应的是 <code>&lt;searchSequence&gt;</code>]]>
    </summary>
    <title>exe4j 打包出来的 exe 报找不到 jvm</title>
    <updated>2026-07-09T08:20:00.000Z</updated>
  </entry>
</feed>
