<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>MCXEN</title>
    <link>https://b.xpai.uk/</link>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, mcx</copyright>
    <lastBuildDate>Tue, 21 Jul 2026 07:57:35 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <atom:link href="https://b.xpai.uk/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>用 Hermes Agent 发博客：从写稿到上线的完整流程</title>
      <link>https://b.xpai.uk/2026/07/21/how-to-publish-blog-with-hermes/</link>
      <description>
        <![CDATA[<p>很多人问我博客怎么更新的，其实现在大部分流程都是 Hermes Agent 在跑。这篇文章讲一下从写稿到上线的完整流程，顺便记录一些踩过的坑。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E5%B7%A5%E5%85%B7/">工具</category>
      <category domain="https://b.xpai.uk/tags/Hermes/">Hermes</category>
      <category domain="https://b.xpai.uk/tags/%E5%8D%9A%E5%AE%A2/">博客</category>
      <category domain="https://b.xpai.uk/tags/%E6%95%99%E7%A8%8B/">教程</category>
      <category domain="https://b.xpai.uk/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/">自动化</category>
      <pubDate>Tue, 21 Jul 2026 12:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>很多人问我博客怎么更新的，其实现在大部分流程都是 Hermes Agent 在跑。这篇文章讲一下从写稿到上线的完整流程，顺便记录一些踩过的坑。</p><span id="more"></span><h2 id="博客架构"><a href="#博客架构" class="headerlink" title="博客架构"></a>博客架构</h2><p>我的博客是 Hexo + GitHub Pages，部署在 <code>mcxen.github.io</code>，自定义域名 <code>b.xpai.uk</code>。</p><table><thead><tr><th>项目</th><th>说明</th></tr></thead><tbody><tr><td>博客源码</td><td><code>~/Documents/OpenSpring/hexo-blog/</code></td></tr><tr><td>文章目录</td><td><code>source/_posts/</code></td></tr><tr><td>图片目录</td><td><code>source/images/</code></td></tr><tr><td>主题</td><td>Anisina（EJS 模板）</td></tr><tr><td>部署</td><td><code>hexo deploy</code> → GitHub Pages</td></tr><tr><td>RSS</td><td><code>/rss.xml</code>（全文输出）</td></tr></tbody></table><h2 id="发布一篇新文章的完整流程"><a href="#发布一篇新文章的完整流程" class="headerlink" title="发布一篇新文章的完整流程"></a>发布一篇新文章的完整流程</h2><h3 id="第一步：写-Markdown"><a href="#第一步：写-Markdown" class="headerlink" title="第一步：写 Markdown"></a>第一步：写 Markdown</h3><p>在 <code>source/_posts/</code> 下创建 <code>.md</code> 文件，加上 frontmatter：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">文章标题</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-07-21 20:00:00</span></span><br><span class="line"><span class="attr">tags:</span> [<span class="string">标签1</span>, <span class="string">标签2</span>]</span><br><span class="line"><span class="attr">categories:</span> <span class="string">分类</span></span><br><span class="line"><span class="attr">header-img:</span> <span class="string">/images/subdir/cover.jpg</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>正文用标准 Markdown，<code>&lt;!-- more --&gt;</code> 之前的内容是首页摘要。</p><h3 id="第二步：封面图"><a href="#第二步：封面图" class="headerlink" title="第二步：封面图"></a>第二步：封面图</h3><p>封面是必须的，主题用它做首页卡片背景和文章头图。</p><p><strong>有正文图片的情况</strong>：用文章第一张图</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">header-img:</span> <span class="string">/images/zhoushan-2026/IMG_1141.jpg</span></span><br></pre></td></tr></table></figure><p><strong>正文没图的情况</strong>：用 Unsplash 随机图，关键词要跟文章内容相关</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># AI/编程类</span></span><br><span class="line"><span class="attr">header-img:</span> <span class="string">https://loremflickr.com/1200/400/artificial-intelligence,programming</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 工具类</span></span><br><span class="line"><span class="attr">header-img:</span> <span class="string">https://loremflickr.com/1200/400/technology,productivity</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 服务器类</span></span><br><span class="line"><span class="attr">header-img:</span> <span class="string">https://loremflickr.com/1200/400/server,datacenter</span></span><br></pre></td></tr></table></figure><p>别用千篇一律的风景图，选跟内容沾边的关键词。</p><h3 id="第三步：图片处理"><a href="#第三步：图片处理" class="headerlink" title="第三步：图片处理"></a>第三步：图片处理</h3><p>iPhone 拍的 HEIC 格式浏览器不认，先转 JPEG：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sips -s format jpeg -Z 1200 /path/to/photo.HEIC --out <span class="built_in">source</span>/images/subdir/photo.jpg</span><br></pre></td></tr></table></figure><p><code>-Z 1200</code> 把长边缩到 1200px，质量够用，文件也不会太大。</p><p>图片放在 <code>source/images/</code> 下，Markdown 里用绝对路径引用：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">![](<span class="link">/images/subdir/photo.jpg</span>)</span><br></pre></td></tr></table></figure><h3 id="第四步：生成静态文件"><a href="#第四步：生成静态文件" class="headerlink" title="第四步：生成静态文件"></a>第四步：生成静态文件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/Documents/OpenSpring/hexo-blog</span><br><span class="line">npx hexo generate</span><br></pre></td></tr></table></figure><p>看到 <code>INFO  N files generated in N ms</code> 就成功了。</p><h3 id="第五步：部署"><a href="#第五步：部署" class="headerlink" title="第五步：部署"></a>第五步：部署</h3><p>因为 GitHub 直连不稳定，需要走代理：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http_proxy=http://127.0.0.1:7897 https_proxy=http://127.0.0.1:7897 npx hexo deploy</span><br></pre></td></tr></table></figure><p>看到 <code>INFO  Deploy done: git</code> 就部署完了。GitHub Pages 可能要缓存一两分钟才更新。</p><h2 id="让-Hermes-来做这些事"><a href="#让-Hermes-来做这些事" class="headerlink" title="让 Hermes 来做这些事"></a>让 Hermes 来做这些事</h2><p>上面的步骤手动也能做，但用 Hermes 的话，只需要一句话：</p><blockquote><p>“帮我写一篇关于 XXX 的博客发一下”</p></blockquote><p>它会：</p><ol><li>根据记忆和上下文整理内容，写出 Markdown</li><li>自动选合适的封面关键词</li><li>如果有图片需求，帮你转换格式</li><li><code>hexo generate</code> + <code>hexo deploy</code> 一键发布</li><li>部署失败会自动重试</li></ol><p>如果你想更新已有的文章，告诉它改什么就行，它会修改后重新部署。</p><h2 id="踩过的坑"><a href="#踩过的坑" class="headerlink" title="踩过的坑"></a>踩过的坑</h2><p><strong>没代理部署失败</strong>：<code>hexo deploy</code> 背后是 <code>git push</code>，不走代理会超时。每次都得带 <code>http_proxy</code>。</p><p><strong>HEIC 格式</strong>：iPhone 照片直接放博客会显示不出来，必须用 <code>sips</code> 转 JPEG。</p><p><strong><code>source/downloads/</code> 被渲染</strong>：这个目录的文件会被 Hexo 处理成 HTML 页面，不会保留原始格式。需要下载的文件放 <code>source/images/</code> 或者用 <code>skip_render</code> 配置。</p><p><strong>敏感信息泄露</strong>：<code>hexo generate</code> 把 Markdown 编译成 HTML，文章里如果有 API key、密码之类的，会直接出现在公开页面上。发布前一定要检查。</p><p><strong>封面图不能为空</strong>：主题的 <code>post-default-img</code> 配置是空的话，没设 <code>header-img</code> 的文章在首页就是光秃秃的。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>用 Hermes 发博客 &#x3D; 告诉它写什么 + 它搞定剩下的。省心。</p><hr><p><em>这篇文章也是 Hermes 帮我写的。</em></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>FxEmbed 与 wsrv.nl：两个低调但实用的网络工具</title>
      <link>https://b.xpai.uk/2026/07/18/fxembed-wsrv-guide/</link>
      <description>
        <![CDATA[<h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>最近整理了一些日常用到的网络工具，其中 <strong>FxEmbed</strong> 和 <strong>wsrv.nl</strong]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      <category domain="https://b.xpai.uk/tags/%E5%B7%A5%E5%85%B7/">工具</category>
      <category domain="https://b.xpai.uk/tags/RSS/">RSS</category>
      <category domain="https://b.xpai.uk/tags/%E5%9B%BE%E7%89%87/">图片</category>
      <category domain="https://b.xpai.uk/tags/%E4%BB%A3%E7%90%86/">代理</category>
      <pubDate>Sat, 18 Jul 2026 14:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>最近整理了一些日常用到的网络工具，其中 <strong>FxEmbed</strong> 和 <strong>wsrv.nl</strong> 是两个虽然知名度不高、但实际场景中非常趁手的服务。写篇文章记录一下用法和案例。</p><h2 id="FxEmbed：让-X-Twitter-和-Bluesky-更好嵌入"><a href="#FxEmbed：让-X-Twitter-和-Bluesky-更好嵌入" class="headerlink" title="FxEmbed：让 X&#x2F;Twitter 和 Bluesky 更好嵌入"></a>FxEmbed：让 X&#x2F;Twitter 和 Bluesky 更好嵌入</h2><p><a href="https://docs.fxembed.com/">FxEmbed</a> 是一个开源项目，提供 fxtwitter.com、fixupx.com、fxbsky.app 等域名，用于优化 X&#x2F;Twitter 和 Bluesky 帖子的嵌入展示。主要功能分为两类。</p><h3 id="1-帖子嵌入"><a href="#1-帖子嵌入" class="headerlink" title="1. 帖子嵌入"></a>1. 帖子嵌入</h3><p>当你把 X 链接粘贴到 Discord、Telegram 等平台时，FxEmbed 会生成更美观的嵌入卡片，支持主题色、媒体预览、翻译等。用法很简单——把域名从 <code>x.com</code> 换成 <code>fxtwitter.com</code> 或 <code>fixupx.com</code> 即可。</p><p>以 <a href="https://x.com/JulieRe18231389/status/2077782204302971226">@JulieRe18231389 的这条推文</a> 为例：</p><blockquote><p>Anyone need a quick launcher for win like this?</p></blockquote><p>这是一个 Windows 快速启动器的演示截图。如果直接分享 x.com 链接在某些平台上可能显示不全，改用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://fxtwitter.com/JulieRe18231389/status/2077782204302971226</span><br></pre></td></tr></table></figure><p>就能获得更完整的嵌入卡片。</p><p><strong>翻译功能</strong>：在 URL 末尾加上两位 ISO 639-1 语言代码，嵌入时会同时显示原文和翻译：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">https://fxtwitter.com/JulieRe18231389/status/2077782204302971226/zh</span><br><span class="line">https://fxtwitter.com/JulieRe18231389/status/2077782204302971226/es</span><br><span class="line">https://fxtwitter.com/JulieRe18231389/status/2077782204302971226/ja</span><br></pre></td></tr></table></figure><h3 id="2-RSS-Atom-订阅"><a href="#2-RSS-Atom-订阅" class="headerlink" title="2. RSS &#x2F; Atom 订阅"></a>2. RSS &#x2F; Atom 订阅</h3><p>这是我最喜欢的功能。FxEmbed 可以为 X 和 Bluesky 用户生成 RSS 2.0 和 Atom 订阅源，无需 API token，直接在阅读器里关注用户动态。</p><p>订阅格式：</p><table><thead><tr><th>服务</th><th>类型</th><th>URL</th></tr></thead><tbody><tr><td>X&#x2F;Twitter</td><td>全部帖子 RSS</td><td><code>https://fxtwitter.com/{handle}/feed.xml</code></td></tr><tr><td>X&#x2F;Twitter</td><td>全部帖子 Atom</td><td><code>https://fxtwitter.com/{handle}/feed.atom.xml</code></td></tr><tr><td>X&#x2F;Twitter</td><td>仅媒体 RSS</td><td><code>https://fxtwitter.com/{handle}/media.xml</code></td></tr><tr><td>Bluesky</td><td>全部帖子 RSS</td><td><code>https://fxbsky.app/profile/{handle}/feed.xml</code></td></tr><tr><td>Bluesky</td><td>仅媒体 RSS</td><td><code>https://fxbsky.app/profile/{handle}/media.xml</code></td></tr></tbody></table><p>可选参数：<code>?count=N</code>（条目数，1-100）、<code>?safe=1</code>（隐藏敏感内容）、<code>?lang=zh</code>（翻译）。</p><p>把链接添加到任意 RSS 阅读器即可订阅。例如订阅 Julie 的最新动态：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://fxtwitter.com/JulieRe18231389/feed.xml</span><br></pre></td></tr></table></figure><p>Bluesky 的媒体源也很有用，只包含带图片&#x2F;视频的帖子，适合做画廊风格的阅读体验。</p><h3 id="3-URL-修饰符"><a href="#3-URL-修饰符" class="headerlink" title="3. URL 修饰符"></a>3. URL 修饰符</h3><p>FxEmbed 还提供一系列 URL 修饰符来控制嵌入行为：</p><ul><li><code>d.{host}</code> — 直接显示媒体文件</li><li><code>t.{host}</code> — 纯文本模式，不加载媒体</li><li><code>g.{host}</code> — 画廊模式</li><li><code>i.{host}</code> — 即时查看模式</li><li><code>m.{host}</code> — 马赛克敏感内容</li></ul><h2 id="wsrv-nl：图片缓存与缩放代理"><a href="#wsrv-nl：图片缓存与缩放代理" class="headerlink" title="wsrv.nl：图片缓存与缩放代理"></a>wsrv.nl：图片缓存与缩放代理</h2><p><a href="https://wsrv.nl/">wsrv.nl</a> 是一个图片缓存&#x2F;缩放服务。你传入图片 URL 和处理参数，它会自动抓取、处理，并通过 Cloudflare CDN 缓存分发。</p><h3 id="核心用法"><a href="#核心用法" class="headerlink" title="核心用法"></a>核心用法</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://wsrv.nl/?url=https://example.com/image.jpg&amp;w=300&amp;h=300</span><br></pre></td></tr></table></figure><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>url</code></td><td>原始图片 URL（必需）</td></tr><tr><td><code>w</code></td><td>目标宽度（px）</td></tr><tr><td><code>h</code></td><td>目标高度（px）</td></tr><tr><td><code>fit</code></td><td>缩放模式：cover、contain、inside、outside</td></tr><tr><td><code>output</code></td><td>输出格式：jpg、png、webp 等</td></tr></tbody></table><h3 id="实际演示"><a href="#实际演示" class="headerlink" title="实际演示"></a>实际演示</h3><p>以 Julie 那条推文配图为例，原图是 2834x1666 的高清大图，体积约 1MB+。下面是不同处理方式的对比。</p><p><strong>原图直接引用</strong>（url 不做任何处理，加载慢且撑爆页面）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg?name=orig</span><br></pre></td></tr></table></figure><p><img src="https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg?name=orig" alt="原图 - 2834x1666"></p><hr><p><strong>用 wsrv.nl 缩放到 800px 宽</strong>（保持比例，CDN 加速）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&amp;w=800</span><br></pre></td></tr></table></figure><p><img src="https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&w=800" alt="wsrv.nl w&#x3D;800"></p><hr><p><strong>用 wsrv.nl 裁剪为 400x300 缩略图</strong>（<code>fit=cover</code>）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&amp;w=400&amp;h=300&amp;fit=cover</span><br></pre></td></tr></table></figure><p><img src="https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&w=400&h=300&fit=cover" alt="wsrv.nl w&#x3D;400 h&#x3D;300 fit&#x3D;cover"></p><hr><p><strong>格式转换</strong>：原图是 JPEG，用 <code>output=webp</code> 转换为 WebP 格式（体积更小）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&amp;w=800&amp;output=webp</span><br></pre></td></tr></table></figure><p><img src="https://wsrv.nl/?url=https://pbs.twimg.com/media/HNXD1PWaQAAblnq.jpg&w=800&output=webp" alt="wsrv.nl output&#x3D;webp"></p><p>可以看到，wsrv.nl 一行 URL 就能完成缩放、裁剪、格式转换，无需部署任何后端服务。</p><h3 id="常见用途"><a href="#常见用途" class="headerlink" title="常见用途"></a>常见用途</h3><ol><li><strong>HTTPS 混合内容修复</strong>：给 HTTPS 网站嵌入 HTTP 图片</li><li><strong>动态缩略图</strong>：不需要自己搭图片处理服务</li><li><strong>格式转换</strong>：SVG&#x2F;PDF 转 PNG 或 WebP</li><li><strong>CDN 加速</strong>：利用 Cloudflare 全球 300+ 节点缓存图片</li><li><strong>动图支持</strong>：支持动态 WebP 和 GIF</li></ol><p>支持格式：JPEG、PNG、BMP、GIF、TIFF、WebP、PDF、SVG。</p><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><p>如果图片 URL 本身带有查询参数，需要对 <code>?</code> 和 <code>&amp;</code> 进行 URL 编码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://wsrv.nl/?url=https%3A%2F%2Fexample.com%2Fimage.jpg%3Ftoken%3Dabc%26size%3Dlarge&amp;w=300</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li><strong>FxEmbed</strong> 解决了社会化媒体内容嵌入和订阅的问题，尤其是无需 API 的 RSS 订阅功能</li><li><strong>wsrv.nl</strong> 是一个轻量的图片代理和处理方案，配合 CDN 效果很好</li></ul><p>两个工具都是开源&#x2F;免费服务，适合个人博客、自动化脚本、信息聚合等场景。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>舟山两日</title>
      <link>https://b.xpai.uk/2026/07/16/zhoushan-trip/</link>
      <description>
        <![CDATA[<p>六月末去了趟舟山，两天一夜，走了几个地方，记录一下。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E6%B8%B8%E8%AE%B0/">游记</category>
      <category domain="https://b.xpai.uk/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      <category domain="https://b.xpai.uk/tags/%E8%88%9F%E5%B1%B1/">舟山</category>
      <category domain="https://b.xpai.uk/tags/%E6%B5%99%E6%B1%9F/">浙江</category>
      <pubDate>Thu, 16 Jul 2026 10:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>六月末去了趟舟山，两天一夜，走了几个地方，记录一下。</p><span id="more"></span><h2 id="第一天"><a href="#第一天" class="headerlink" title="第一天"></a>第一天</h2><p>到了已经快中午了，先在商业区找了家店吃点东西。宁海食府附近，选了个KFC——旅行的第一顿往往不是当地特色，而是你熟悉的连锁店，大概是舟山行的一个小注脚。</p><p><img src="/images/zhoushan-2026/IMG_1141.jpg"></p><p>商业区挺热闹，李府食府门口有黄色花茎艺术雕塑。</p><p><img src="/images/zhoushan-2026/IMG_1142.jpg"></p><p>下午去了<strong>长乔海洋国际旅游度假区</strong>。那天是阴天，云压得很低，园区建筑有欧式童话风格，塔楼、暖色外墙、修剪整齐的草坪。地方挺大，人不多，逛起来舒服。路上看到山坡上”浙江舟山群岛新区欢迎您”的红色大字。</p><p><img src="/images/zhoushan-2026/IMG_1146.jpg"></p><p>园区内有港口，停泊着船只和起重机，水面开阔。</p><p><img src="/images/zhoushan-2026/IMG_1154.jpg"></p><p>度假区建筑是欧式城堡风格，广场上有鲸鱼雕塑，入口有花坛。</p><p><img src="/images/zhoushan-2026/IMG_1161.jpg"><br><img src="/images/zhoushan-2026/IMG_1162.jpg"></p><p>从度假区出来，又绕到一个依山而建的营地。山脚下草坪开阔，搭了些帐篷和休闲设施，有人撑着伞在林间散步。舟山的山不高，但植被茂密，空气里是南方特有的潮润。</p><p><img src="/images/zhoushan-2026/IMG_1170.jpg"></p><p>晚上的演出有点意思——观音主题的实景表演。舞台灯光打得很好，演员扮作观音形象，白衣素裹，两侧侍者持莲花灯，僧人列队，氛围肃穆又有些梦幻。</p><p><img src="/images/zhoushan-2026/IMG_1229.jpg"><br><img src="/images/zhoushan-2026/IMG_1237.jpg"></p><h2 id="第二天"><a href="#第二天" class="headerlink" title="第二天"></a>第二天</h2><p>早起去了码头。海是灰蓝色的，不是那种旅游宣传片里的碧蓝，但更真实。码头的围栏上有创意涂鸦，渔船挂着国旗，斑马线延伸到海边——一个有人生活痕迹的港口。</p><p><img src="/images/zhoushan-2026/IMG_1249.jpg"></p><p>退潮后的泥滩很有意思，满地小孔洞，小螃蟹和弹涂鱼在泥里钻来钻去，潮间带的生命力远比想象中旺盛。</p><p><img src="/images/zhoushan-2026/IMG_1254.jpg"></p><p>下午路过宁波，去了趟<strong>天一阁</strong>。中国现存最古老的私人藏书楼，明代范钦建于嘉靖年间。阁名”天一”取自”天一生水”——以水克火，护佑藏书。</p><p>穿过月湖，先到的是天一阁外的文创街区。青瓦坡顶，灰色遮阳篷下是卖伴手礼的小店，门头上挂着”甬城纪念品”的招牌。阴天的光线下，老建筑的细节特别清楚。</p><p><img src="/images/zhoushan-2026/IMG_1261.jpg"></p><p>天一阁本身不大，但园林极其精致。黛瓦翘角的墙上爬满藤蔓，石板地透着湿气。一株老朴树，枝干上长了青苔，旁边是棕榈和阔叶植物。园中一池碧水，假山叠石，树影入波，是典型的江南园林景致。</p><p><img src="/images/zhoushan-2026/IMG_1265.jpg"></p><p>最后一站是园内的<strong>中国地方志珍藏馆</strong>。木质结构的老建筑，深红漆面有些斑驳，门楣上挂着匾额。里面陈列着全国各地的地方志和书画，与天一阁的藏书气质一脉相承。</p><p><img src="/images/zhoushan-2026/IMG_1269.jpg"><br><img src="/images/zhoushan-2026/IMG_1320.jpg"></p><h2 id="关于照片"><a href="#关于照片" class="headerlink" title="关于照片"></a>关于照片</h2><p>这次的照片都是用手机拍的，用小米 MiMo v2.5 看了每张照片的内容来识别场景——效果比预期的好，能认出具体的景点名称和建筑细节。照片本身没带 GPS 信息，所以没法用高德 API 反查定位，纯靠 AI 看图描述。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Pi Coding Agent 扩展生态指南 — 从安装到实战</title>
      <link>https://b.xpai.uk/2026/07/10/pi-agent-extensions-guide/</link>
      <description>
        <![CDATA[<p>上次分享了怎么给 <a href="https://pi.dev/">pi coding agent</a> 配自定义 provider，这次聊聊它的扩展生态——一个被低估但非常强大的特性。</p>
<blockquote>
<p><strong>免责声明</strong>：本文涉及的 API Key 等敏感信息均已脱敏处理。实际使用请自行替换。</p>
</blockquote>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/">效率工具</category>
      <category domain="https://b.xpai.uk/tags/pi/">pi</category>
      <category domain="https://b.xpai.uk/tags/%E9%85%8D%E7%BD%AE/">配置</category>
      <category domain="https://b.xpai.uk/tags/%E6%89%A9%E5%B1%95/">扩展</category>
      <pubDate>Fri, 10 Jul 2026 02:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>上次分享了怎么给 <a href="https://pi.dev/">pi coding agent</a> 配自定义 provider，这次聊聊它的扩展生态——一个被低估但非常强大的特性。</p><blockquote><p><strong>免责声明</strong>：本文涉及的 API Key 等敏感信息均已脱敏处理。实际使用请自行替换。</p></blockquote><span id="more"></span><h2 id="为什么需要扩展"><a href="#为什么需要扩展" class="headerlink" title="为什么需要扩展"></a>为什么需要扩展</h2><p>Pi 的设计哲学是”最小核心 + 可插拔”——它本身只有最基本的 read&#x2F;write&#x2F;edit&#x2F;bash 工具，但通过扩展机制，你可以给它加上任意能力：子代理、规划模式、持久记忆、权限保护、LSP 诊断……几乎 Claude Code 有的功能，Pi 都能通过扩展补上。</p><p>而且扩展是用 TypeScript 写的，本质上就是一个 npm 包。你甚至可以问 Pi 自己写一个扩展。</p><h2 id="安装扩展"><a href="#安装扩展" class="headerlink" title="安装扩展"></a>安装扩展</h2><p>Pi 的扩展通过 <code>pi install</code> 命令安装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi install npm:@org/package-name</span><br></pre></td></tr></table></figure><p>安装后需要在 Pi 交互界面中执行 <code>/reload</code> 让扩展生效。不改配置文件，不重启终端，非常干净。</p><p>查看已安装的扩展：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi list</span><br></pre></td></tr></table></figure><h2 id="我装的扩展清单"><a href="#我装的扩展清单" class="headerlink" title="我装的扩展清单"></a>我装的扩展清单</h2><h3 id="🎯-Goal-模式-—-长期目标追踪"><a href="#🎯-Goal-模式-—-长期目标追踪" class="headerlink" title="🎯 Goal 模式 — 长期目标追踪"></a>🎯 Goal 模式 — 长期目标追踪</h3><p>两个目标管理扩展，二选一即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方案 A：基础版</span></span><br><span class="line">pi install npm:pi-agent-goal</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方案 B：增强版（推荐，带 setup 访谈）</span></span><br><span class="line">pi install npm:@qhn/pi-goal</span><br></pre></td></tr></table></figure><p><code>@qhn/pi-goal</code> 会在设置目标前先进行一个”访谈”——和用户对齐 outcome、完成标准、决策风格、ask-before 边界——然后再开始自主工作。体验上更接近 Claude Code 的 plan 模式。</p><p>注意两个扩展都注册了 <code>/goal</code> 命令，不要同时装。</p><h3 id="🔒-权限保护-—-防误操作"><a href="#🔒-权限保护-—-防误操作" class="headerlink" title="🔒 权限保护 — 防误操作"></a>🔒 权限保护 — 防误操作</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi install npm:@gotgenes/pi-permission-system</span><br></pre></td></tr></table></figure><p>这个扩展给 Pi 加了一层权限门控。当模型要执行删除、覆盖等危险操作时，会先请求确认。适合在需要保护的代码库中使用，防止 AI 不小心把不该删的文件删了。</p><h3 id="📋-Todo-浮动层"><a href="#📋-Todo-浮动层" class="headerlink" title="📋 Todo 浮动层"></a>📋 Todo 浮动层</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi install npm:@juicesharp/rpiv-todo</span><br></pre></td></tr></table></figure><p>在 Pi 界面中增加一个 todo 列表浮动层，由模型自己维护。多步任务时特别有用——模型可以看到还剩哪些步骤没做，不会跑偏。</p><h3 id="📝-任务规划"><a href="#📝-任务规划" class="headerlink" title="📝 任务规划"></a>📝 任务规划</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi install npm:@mjasnikovs/pi-task</span><br></pre></td></tr></table></figure><p>确定性任务规划 + 规范编排。适合需要严格按步骤执行的场景，Pi 会先生成计划、再逐步执行、最后验证结果。支持将任务分解为可追踪的子任务。</p><h3 id="🧠-持久记忆"><a href="#🧠-持久记忆" class="headerlink" title="🧠 持久记忆"></a>🧠 持久记忆</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi install npm:@remnic/plugin-pi</span><br></pre></td></tr></table></figure><p>让 Pi 拥有跨会话记忆能力。之前聊过的上下文、做过的工作会被记住，下次开启新会话时不需要重新交代背景。</p><h3 id="🔧-其他推荐扩展"><a href="#🔧-其他推荐扩展" class="headerlink" title="🔧 其他推荐扩展"></a>🔧 其他推荐扩展</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 子代理</span></span><br><span class="line">pi install npm:pi-subagents                <span class="comment"># 并行/链式任务委派</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 联网能力</span></span><br><span class="line">pi install npm:pi-web-access               <span class="comment"># 联网搜索、GitHub 克隆、PDF/YouTube 提取</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 代码智能</span></span><br><span class="line">pi install npm:pi-lens                     <span class="comment"># LSP 实时代码反馈（lint、type-check）</span></span><br><span class="line">pi install npm:@ff-labs/pi-fff             <span class="comment"># 模糊文件/内容搜索</span></span><br><span class="line">pi install npm:pi-simplify                 <span class="comment"># 自动审查并简化新改代码</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 上下文管理</span></span><br><span class="line">pi install npm:context-mode                <span class="comment"># MCP 插件，压缩 context 省 98%</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 导航</span></span><br><span class="line">pi install npm:@ayulab/pi-rewind           <span class="comment"># 检查点导航</span></span><br><span class="line">pi install npm:@quintinshaw/pi-dynamic-workflows  <span class="comment"># Claude Code 式动态工作流</span></span><br></pre></td></tr></table></figure><h2 id="完整的配置结构"><a href="#完整的配置结构" class="headerlink" title="完整的配置结构"></a>完整的配置结构</h2><p>装完扩展后，<code>~/.pi/agent/</code> 目录结构大概是这样的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">~/.pi/agent/</span><br><span class="line">├── auth.json           # API Key 存储</span><br><span class="line">├── models.json         # 自定义模型提供商</span><br><span class="line">├── settings.json       # 全局设置</span><br><span class="line">├── sessions/           # 会话记录</span><br><span class="line">├── npm/                # 扩展包安装目录</span><br><span class="line">│   └── node_modules/</span><br><span class="line">│       ├── @qhn/pi-goal/</span><br><span class="line">│       ├── @gotgenes/pi-permission-system/</span><br><span class="line">│       ├── @juicesharp/rpiv-todo/</span><br><span class="line">│       ├── @mjasnikovs/pi-task/</span><br><span class="line">│       └── @remnic/plugin-pi/</span><br><span class="line">└── trust.json          # 项目信任记录</span><br></pre></td></tr></table></figure><h2 id="自定义模型提供商配置"><a href="#自定义模型提供商配置" class="headerlink" title="自定义模型提供商配置"></a>自定义模型提供商配置</h2><p>我用的 krill 示例（<code>~/.pi/agent/models.json</code>）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;providers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;krill&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Krill API&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;baseUrl&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://api.krill-ai.com/codex/v1&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;api&quot;</span><span class="punctuation">:</span> <span class="string">&quot;openai-completions&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;apiKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$KRILL_API_KEY&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;models&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;deepseek-v4-flash&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.5&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.6-sol&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.6-terra&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.6-luna&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kimi-k2.7-code&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gemini-3.1-pro-preview&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">1048576</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <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">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>API Key 通过环境变量 <code>KRILL_API_KEY</code> 注入，或写到 <code>auth.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;krill&quot;</span><span class="punctuation">:</span> <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;api_key&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;your-api-key-here&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="tmux-Pi-后台工作流"><a href="#tmux-Pi-后台工作流" class="headerlink" title="tmux + Pi 后台工作流"></a>tmux + Pi 后台工作流</h2><p>Pi 配合 tmux 可以长时间在后台运行，适合做大任务：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动</span></span><br><span class="line">tmux new-session -d -s pi-session -c ~/project</span><br><span class="line">tmux send-keys -t pi-session <span class="string">&quot;pi --model krill/gpt-5.6-sol&quot;</span> Enter</span><br><span class="line"></span><br><span class="line"><span class="comment"># 发任务</span></span><br><span class="line">tmux send-keys -t pi-session <span class="string">&quot;你的任务描述&quot;</span> Enter</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看进度</span></span><br><span class="line">tmux capture-pane -t pi-session -p -S -30 | <span class="built_in">tail</span> -20</span><br><span class="line"></span><br><span class="line"><span class="comment"># 附着查看</span></span><br><span class="line">tmux attach -t pi-session</span><br></pre></td></tr></table></figure><p>建议在 <code>~/.tmux.conf</code> 中加上 <code>set -g extended-keys on</code>，否则 Pi 的某些快捷键在 tmux 中会报 warning。</p><h2 id="小技巧"><a href="#小技巧" class="headerlink" title="小技巧"></a>小技巧</h2><ol><li><strong>扩展不要一次装太多</strong> — 每个扩展都会增加 context 开销。先装最需要的，不够再加</li><li><strong>两个 goal 扩展冲突</strong> — 不要同时装 <code>pi-agent-goal</code> 和 <code>@qhn/pi-goal</code>，它们都注册了 <code>/goal</code> 命令。二选一</li><li><strong>models.json 热重载</strong> — 编辑完不需要重启 Pi，<code>/model</code> 或 <code>--model</code> 时会自动重新加载</li><li><strong>API Key 别写死在 JSON 里</strong> — 用 <code>$ENV_VAR</code> 引用环境变量，安全且方便切换</li><li><strong>多模型切换</strong> — Pi 支持 Ctrl+L 快速切换模型，可以按任务类型切换（重任务用 Sol，轻量用 Luna）</li></ol><h2 id="资源"><a href="#资源" class="headerlink" title="资源"></a>资源</h2><ul><li>可以在 <a href="https://pi.dev/packages">pi.dev&#x2F;packages</a> 查看完整扩展目录</li><li>也可以在 npm 搜索 <code>keywords:pi-package</code> 发现更多扩展</li><li>我的完整 Skill 配置文件：<a href="/downloads/pi-coding-agent-skill.md">下载 pi-coding-agent-skill.md</a>（Hermes Agent skill 格式，可放在 <code>~/.hermes/skills/</code> 下使用）</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>Pi 的扩展生态远比我想象的成熟。从 goal 模式到子代理，从权限保护到持久记忆，大部分我想要的 Claude Code 功能都能通过扩展补上，而且安装过程极其简单——一行命令 + <code>/reload</code> 搞定。</p><p>如果你也在用 AI 编码助手，不妨试试 Pi + 扩展的组合，说不定会有惊喜。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>配置 pi agent 自定义模型提供商</title>
      <link>https://b.xpai.uk/2026/07/08/pi-agent-custom-provider/</link>
      <description>
        <![CDATA[<p>最近在折腾 <code>pi</code> 这个 AI 编码助手（<code>@earendil-works/pi-coding-agent</code>）——一个类似 Claude Code、Codex 的命令行工具，支持多种 AI 模型后端，可以用自然语言驱动写代码、执行命令。</p>
<p>刚好手头有一些第三方 API 额度，所以记录一下怎么给它配一个自定义（OpenAI 兼容）的模型提供商。</p>
<blockquote>
<p><strong>免责声明</strong>：本文涉及的 API Key 等敏感信息均已脱敏处理。</p>
</blockquote>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/">效率工具</category>
      <category domain="https://b.xpai.uk/tags/pi/">pi</category>
      <category domain="https://b.xpai.uk/tags/%E9%85%8D%E7%BD%AE/">配置</category>
      <pubDate>Wed, 08 Jul 2026 10:30:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近在折腾 <code>pi</code> 这个 AI 编码助手（<code>@earendil-works/pi-coding-agent</code>）——一个类似 Claude Code、Codex 的命令行工具，支持多种 AI 模型后端，可以用自然语言驱动写代码、执行命令。</p><p>刚好手头有一些第三方 API 额度，所以记录一下怎么给它配一个自定义（OpenAI 兼容）的模型提供商。</p><blockquote><p><strong>免责声明</strong>：本文涉及的 API Key 等敏感信息均已脱敏处理。</p></blockquote><span id="more"></span><h2 id="pi-的架构"><a href="#pi-的架构" class="headerlink" title="pi 的架构"></a>pi 的架构</h2><p><code>pi</code> 的模型管理分两层：</p><ul><li><strong>内置 provider</strong> — <code>openai</code>、<code>anthropic</code>、<code>deepseek</code>、<code>openrouter</code>、<code>google</code> 等几十个，开箱即用</li><li><strong>自定义 provider</strong> — 通过 <code>~/.pi/agent/models.json</code> 配置，支持 OpenAI 兼容接口</li></ul><p>自定义配置的 schema 官方文档在 <a href="https://pi.dev/models">pi.dev&#x2F;models</a>。</p><h2 id="配置步骤"><a href="#配置步骤" class="headerlink" title="配置步骤"></a>配置步骤</h2><h3 id="1-准备-API-Key"><a href="#1-准备-API-Key" class="headerlink" title="1. 准备 API Key"></a>1. 准备 API Key</h3><p>把 API Key 写入环境变量（推荐，避免明文写在配置文件中）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ~/.zshrc</span></span><br><span class="line"><span class="built_in">export</span> KRILL_API_KEY=your_api_key_here</span><br></pre></td></tr></table></figure><h3 id="2-创建-models-json"><a href="#2-创建-models-json" class="headerlink" title="2. 创建 models.json"></a>2. 创建 models.json</h3><p><code>pi</code> 的配置目录在 <code>~/.pi/agent/</code>，创建 <code>models.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;providers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;krill&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Krill API&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;baseUrl&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://api.krill-ai.com/codex/v1&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;api&quot;</span><span class="punctuation">:</span> <span class="string">&quot;openai-completions&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;apiKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$KRILL_API_KEY&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;models&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;deepseek-v4-flash&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;deepseek-v4-pro&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.4&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-5.5&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">256000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kimi-k2.7-code&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span><span class="punctuation">,</span> <span class="attr">&quot;reasoning&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gemini-3.1-pro-preview&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">1048576</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">32768</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;qwen3.7-plus&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">128000</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span> <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;grok-4.5&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;contextWindow&quot;</span><span class="punctuation">:</span> <span class="number">131072</span><span class="punctuation">,</span> <span class="attr">&quot;maxTokens&quot;</span><span class="punctuation">:</span> <span class="number">16384</span> <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">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>关键字段说明：</p><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>baseUrl</code></td><td>API 端点，支持任何 OpenAI 兼容的接口</td></tr><tr><td><code>api</code></td><td>协议类型，OpenAI 兼容填 <code>openai-completions</code></td></tr><tr><td><code>apiKey</code></td><td>支持 <code>$ENV_VAR</code> 语法引用环境变量</td></tr><tr><td><code>reasoning</code></td><td>是否支持思维链&#x2F;推理能力</td></tr></tbody></table><h3 id="3-验证配置"><a href="#3-验证配置" class="headerlink" title="3. 验证配置"></a>3. 验证配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 列出所有模型</span></span><br><span class="line">pi --list-models</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出中会看到 krill provider 和配置的模型列表</span></span><br><span class="line"><span class="comment"># Auth 列显示 yes 表示 API Key 已就绪</span></span><br></pre></td></tr></table></figure><p>测试调用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pi --provider krill --model deepseek-v4-flash --<span class="built_in">print</span> <span class="string">&quot;hello&quot;</span></span><br></pre></td></tr></table></figure><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p><code>pi</code> 的模型加载流程：</p><ol><li>启动时读取 <code>~/.pi/agent/models.json</code></li><li>合并内置 provider 和自定义 provider</li><li>根据 <code>apiKey</code> 字段中的 <code>$ENV_VAR</code> 读取环境变量</li><li>用 <code>openai-completions</code> API 模块创建 OpenAI 客户端</li><li>请求时 <code>baseURL</code> 指向 <code>model.baseUrl</code>，<code>model</code> 参数传 <code>model.id</code></li></ol><p>所以只要是 OpenAI 兼容的接口——无论是 Krill、OpenRouter、还是本地跑的 vLLM、Ollama——都可以用同样的方式接入。</p><h2 id="一点经验"><a href="#一点经验" class="headerlink" title="一点经验"></a>一点经验</h2><ul><li><strong>API Key 别写死在 JSON 里</strong> — 用 <code>$ENV_VAR</code> 引用环境变量，安全且方便切换</li><li><strong>cost 字段不填也行</strong> — 默认 <code>0</code>，不影响使用</li><li><strong>contextWindow 和 maxTokens</strong> — 按模型的实际能力填，不影响调用但对规划有帮助</li><li><strong><code>reasoning: true</code></strong> 开启思维链，适合 deepseek、kimi 等支持推理的模型</li><li>如果遇到 <code>insufficient balance</code> — 那是 API 提供商侧欠费了，检查账户余额就行</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p><code>pi</code> 的自定义 provider 机制设计得很灵活，一个 <code>models.json</code> 就能接任意 OpenAI 兼容接口。这种”配置驱动”的方式比硬编码更干净，改模型只要改配置文件就行，不用动工具本身。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>AI 为什么没有替代软件工程师</title>
      <link>https://b.xpai.uk/2026/07/08/ai-not-replacing-engineers/</link>
      <description>
        <![CDATA[<p>最近读了一篇很好的文章 “Why AI hasn’t replaced software engineers, and won’t”（AI Snake Oil 通讯），结合自己的一些思考，整理一下核心观点。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E6%80%9D%E8%80%83/">思考</category>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/">软件工程</category>
      <category domain="https://b.xpai.uk/tags/%E7%BB%8F%E6%B5%8E/">经济</category>
      <pubDate>Wed, 08 Jul 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近读了一篇很好的文章 “Why AI hasn’t replaced software engineers, and won’t”（AI Snake Oil 通讯），结合自己的一些思考，整理一下核心观点。</p><span id="more"></span><h2 id="一层层剥开”AI-替代程序员”的叙事"><a href="#一层层剥开”AI-替代程序员”的叙事" class="headerlink" title="一层层剥开”AI 替代程序员”的叙事"></a>一层层剥开”AI 替代程序员”的叙事</h2><h3 id="1-那些”AI-导致裁员”的新闻，经不起推敲"><a href="#1-那些”AI-导致裁员”的新闻，经不起推敲" class="headerlink" title="1. 那些”AI 导致裁员”的新闻，经不起推敲"></a>1. 那些”AI 导致裁员”的新闻，经不起推敲</h3><p>文章追踪了几个著名的案例：</p><table><thead><tr><th>公司</th><th>官方说法</th><th>实际情况</th></tr></thead><tbody><tr><td><strong>Block</strong>（Square）</td><td>CEO Jack Dorsey 说 AI 让团队更扁平，裁员 4000</td><td>疫情间招了三倍人，财务压力才是真因</td></tr><tr><td><strong>Snap</strong></td><td>说 AI 写了 65% 的代码，裁员 1000</td><td>实际上是被激进投资者逼着降成本，公司上市以来年年亏损</td></tr><tr><td><strong>Intuit</strong></td><td>裁员 3000，同时签了 AI 合作</td><td>CEO 自己否认跟 AI 有关，说是管理层级太多</td></tr></tbody></table><p>这不是挑出来的特例。文章调查发现了一个普遍现象：</p><blockquote><p>59% 的美国招聘经理承认，他们拿 AI 当裁员的借口，因为对股东说”AI 替代了人”比说”我们财务紧张”好听得多。</p></blockquote><p>Forrester 的分析师说得很直白：<strong>“问他们有没有成熟的 AI 产品能顶上那些岗位，十有八九答案是没有，甚至还没开始做。”</strong></p><h3 id="2-“三明治”模型：为什么写代码从来不是瓶颈"><a href="#2-“三明治”模型：为什么写代码从来不是瓶颈" class="headerlink" title="2. “三明治”模型：为什么写代码从来不是瓶颈"></a>2. “三明治”模型：为什么写代码从来不是瓶颈</h3><p>文章提出了一个很好的框架——<strong>软件开发的”决定—执行—交付”三明治</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────┐</span><br><span class="line">│    决定层 (Decide)       │  ← 理解需求、拆解问题、方案设计</span><br><span class="line">├─────────────────────────┤</span><br><span class="line">│    执行层 (Execute)      │  ← 写代码（AI 正在压缩的部分）</span><br><span class="line">├─────────────────────────┤</span><br><span class="line">│    交付层 (Deliver)      │  ← 测试、集成、部署、运维</span><br><span class="line">└─────────────────────────┘</span><br></pre></td></tr></table></figure><p>AI 压缩的是中间的”执行层”。但两端的”决定”和”交付”几乎没变。</p><p>一组关键数据：GitHub 上 10 万开发者的研究发现，AI 让代码产出提高了 <strong>8 倍</strong>，但最终发布只增加了 <strong>30%</strong>。这 30% vs 800% 的差距，就是人和 AI 之间不可替代的部分。</p><h3 id="3-为什么员工总数反而可能增加"><a href="#3-为什么员工总数反而可能增加" class="headerlink" title="3. 为什么员工总数反而可能增加"></a>3. 为什么员工总数反而可能增加</h3><p>这就回到了经济学的核心逻辑。不是简单的”写代码变快了所以不需要那么多人”，而是：</p><p><strong>软件需求有高价格弹性。当软件变便宜了，市场会需要多得多的软件。</strong></p><p>举个例子：</p><ul><li>以前开发一个功能要 10 人月，成本 50 万，公司算不过账，不做了</li><li>现在 AI 辅助下只要 2 人月，成本 10 万，公司觉得划算，决定做</li><li><strong>净效果：需要的人更多了</strong></li></ul><p>这个逻辑有个响亮的经济学名字——<strong>杰文斯悖论（Jevons’ Paradox）</strong>：</p><blockquote><p>蒸汽机效率提高后，每吨煤能做的功变多了，但煤炭总消耗反而上升了——因为煤变”划算”了，大家用得更狠。</p></blockquote><p>放到软件行业就是：AI 让每个工程师的产出变高，但市场对软件的总需求增长更快。</p><h3 id="4-历史不会撒谎"><a href="#4-历史不会撒谎" class="headerlink" title="4. 历史不会撒谎"></a>4. 历史不会撒谎</h3><table><thead><tr><th>历史事件</th><th>当时的主流恐惧</th><th>实际结果</th></tr></thead><tbody><tr><td>高级语言出现</td><td>不需要那么多程序员了</td><td>程序员数量翻了几十倍</td></tr><tr><td>开源运动</td><td>商业软件公司要完</td><td>催生了全新的产业</td></tr><tr><td>云计算</td><td>IT 运维人员要失业</td><td>运维需求暴涨</td></tr><tr><td><strong>AI 辅助编程</strong></td><td><strong>程序员要被替代了</strong></td><td><strong>？</strong></td></tr></tbody></table><p>每一轮”技术替代人”的恐慌过后，事实都证明：技术不是替代人，而是重新定义人的工作内容，并且总需求在增长。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>这不是盲目乐观。文章也承认：<strong>即使总需求在增长，个体软件工程师的职业生涯可能会更动荡。</strong> 比如：</p><ul><li>初级岗位可能减少，因为 AI 补了部分”执行”能力</li><li>纯”码代码”的岗位价值下降</li><li>但懂业务、能决策、能交付的人会更值钱</li></ul><p>最让我有共鸣的是这句话：</p><blockquote><p>AI 压缩了三明治的中间层，但两端的”决定”和”交付”几乎没有变化。而真正重要的是两端。</p></blockquote><hr><p><em>参考：Arvind Narayanan &amp; Sayash Kapoor, “Why AI hasn’t replaced software engineers, and won’t”, AI Snake Oil, Jun 2026</em></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hermes Agent 帮我做了些什么</title>
      <link>https://b.xpai.uk/2026/07/06/hermes-agent-helps/</link>
      <description>
        <![CDATA[<p>深度使用 <a href="https://hermes-agent.nousresearch.com/">Hermes Agent</a> 已经快一年了，从最初的尝鲜到现在离不开。这篇文章更新一下它帮我搞定的事情，按类别整理。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E5%B7%A5%E5%85%B7/">工具</category>
      <category domain="https://b.xpai.uk/tags/Hermes/">Hermes</category>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/">效率工具</category>
      <pubDate>Mon, 06 Jul 2026 15:30:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>深度使用 <a href="https://hermes-agent.nousresearch.com/">Hermes Agent</a> 已经快一年了，从最初的尝鲜到现在离不开。这篇文章更新一下它帮我搞定的事情，按类别整理。</p><span id="more"></span><h2 id="项目发布一条龙"><a href="#项目发布一条龙" class="headerlink" title="项目发布一条龙"></a>项目发布一条龙</h2><p>最频繁的工作就是给 <a href="https://github.com/mcxen/qx">Qx</a> 项目做发布。从 <code>v0.4.36</code> 到现在的 <code>v0.6.5</code>，发布了几十个版本：</p><ul><li>自动检测未提交的变更</li><li>更新版本号（package.json、Cargo.toml、tauri.conf.json 三处同步）</li><li>运行 <code>tsc --noEmit</code> 和 <code>cargo check</code> 验证编译</li><li>打 tag、推送到 GitHub</li><li>一条命令搞定，遇到网络波动会自动重试</li></ul><p>另一个项目 <a href="https://github.com/mcxen/shelfy">Shelfy</a>（Tauri 文件整理工具）也是同样的流程。后来还搞定了 Homebrew Cask 分发：<code>brew tap mcxen/qx &amp;&amp; brew install --cask mcxen/qx/qx</code>。</p><h2 id="frp-隧道和远程管理"><a href="#frp-隧道和远程管理" class="headerlink" title="frp 隧道和远程管理"></a>frp 隧道和远程管理</h2><p>需要在外面连回家里的 Mac，Hermes 帮我：</p><ul><li>检查 frps 服务器（121.40.88.21）是否在线</li><li>配置 frpc 客户端，写好配置文件</li><li><code>brew services start frpc</code> 开机自启</li><li>SSH 到远程 Linux 服务器执行各种运维命令</li></ul><p>全程不需要手动编辑配置文件，自然语言描述需求就行。</p><h2 id="qodercli-自动化"><a href="#qodercli-自动化" class="headerlink" title="qodercli 自动化"></a>qodercli 自动化</h2><p>Qx 项目里很多代码改动是通过 qodercli（AI 编码 CLI）完成的，Hermes 负责在 tmux 里操作它：</p><ul><li><strong>NSPanel 悬浮面板</strong> – macOS 原生无焦点面板实现，这是 Qx 的核心交互</li><li><strong>主题切换卡顿修复</strong> – 排查 CSS 变量在暗黑模式下缺失的问题</li><li><strong>Markdown 渲染升级</strong> – 从 react-markdown + shiki 换成更轻量的方案</li><li><strong>流式输出卡死修复</strong> – 重构 Rust 后端的流式处理逻辑</li><li><strong>设置面板布局修复</strong> – 导航项固定行高 + label 截断</li><li><strong>GitHub Actions 步骤进度</strong> – 解析 CI 页面的 HTML 结构，显示真实的步骤完成情况</li></ul><h2 id="Memos-MCP-服务器"><a href="#Memos-MCP-服务器" class="headerlink" title="Memos MCP 服务器"></a>Memos MCP 服务器</h2><p>自己搞了个 <a href="https://usememos.com/">Memos</a> 实例（121.40.88.21:12009），Hermes 帮我：</p><ul><li>搭建 FastMCP 服务器（<code>~/Documents/OpenSpring/memos-mcp/</code>）</li><li>直接通过 API 读写 memo 笔记</li><li>搜索已有笔记做信息关联</li></ul><h2 id="博客维护"><a href="#博客维护" class="headerlink" title="博客维护"></a>博客维护</h2><p>Hexo 博客（mcxen.github.io）的日常维护基本都是 Hermes 在跑：</p><ul><li>写文章、整理内容</li><li>HEIC 照片转 JPEG、图片放到 <code>source/images/</code></li><li><code>hexo generate</code> + <code>hexo deploy</code>（走代理 7897）</li><li>自动给没有封面的文章选 Unsplash 关键词配封面</li><li>RSS 全文订阅配置</li></ul><h2 id="网络工具和资源下载"><a href="#网络工具和资源下载" class="headerlink" title="网络工具和资源下载"></a>网络工具和资源下载</h2><ul><li><strong>X&#x2F;Twitter 视频</strong> – 用 yt-dlp 下载指定推文视频</li><li><strong>720yun 全景图</strong> – 下载全景切片并合成为等距柱状投影图</li><li><strong>NAS 文件管理</strong> – 通过 SMB 挂载飞牛 NAS（10.0.0.124），排查 macOS SMB 连接问题</li><li><strong>Cloudflare Pages</strong> – 部署静态站点，配置自定义域名</li></ul><h2 id="DevOps-和-CI"><a href="#DevOps-和-CI" class="headerlink" title="DevOps 和 CI"></a>DevOps 和 CI</h2><ul><li><strong>Cloudflare Workers</strong> – 搭建战术终端风格的 Web 界面</li><li><strong>Caddy 反代 + ICP 备案</strong> – 踩坑记录，跟阿里云和 Cloudflare 的 DNS 配置斗智斗勇</li><li><strong>GitHub Actions</strong> – 解析 CI 运行详情页的 HTML 结构，实现真实的步骤进度展示</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>AI 助手已经从聊天玩具变成了真的能干活的生产力工具。从项目发布、代码重构、服务器配置到内容创作，基本上说”帮我搞一下”，它就能自己折腾出来。</p><p>最大的变化是：我现在遇到问题的第一反应不是去搜，而是让 Hermes 来处理。</p><hr><p><em>这篇文章本身就是 Hermes 根据记忆和会话记录自动整理成文的。</em></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>我的 AI 助手 -- Hermes Agent 日常使用记录</title>
      <link>https://b.xpai.uk/2026/07/06/hermes-agent-workflow/</link>
      <description>
        <![CDATA[<p>从去年开始用 Hermes Agent（Nous Research 出品的 CLI AI 智能体），到现在已经深度嵌入了日常开发和工作流。这篇整理一下它的主要使用场景，也给好奇的人参考。</p>
<blockquote>
<p>本文涉及的所有敏感信息（IP、密码、Token、服务器配置等）均已脱敏处理。</p>
</blockquote>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/">效率工具</category>
      <category domain="https://b.xpai.uk/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/">工作流</category>
      <pubDate>Mon, 06 Jul 2026 11:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>从去年开始用 Hermes Agent（Nous Research 出品的 CLI AI 智能体），到现在已经深度嵌入了日常开发和工作流。这篇整理一下它的主要使用场景，也给好奇的人参考。</p><blockquote><p>本文涉及的所有敏感信息（IP、密码、Token、服务器配置等）均已脱敏处理。</p></blockquote><span id="more"></span><h2 id="什么是-Hermes-Agent"><a href="#什么是-Hermes-Agent" class="headerlink" title="什么是 Hermes Agent"></a>什么是 Hermes Agent</h2><p>Hermes Agent 是一个开源的 CLI AI 智能体，运行在终端里，能直接操控 macOS&#x2F;Linux 系统——读写文件、执行命令、管理 git、调用 API、搜索网页。它不是”聊天机器人”，而是一个能动手干活的 AI 助手。</p><p>它有完整的工具链和技能系统，可以安装各种”技能”来扩展能力。支持多种大模型后端，我主要用 DeepSeek 和 mimo。</p><h2 id="场景一：日常开发辅助"><a href="#场景一：日常开发辅助" class="headerlink" title="场景一：日常开发辅助"></a>场景一：日常开发辅助</h2><h3 id="代码编写与调试"><a href="#代码编写与调试" class="headerlink" title="代码编写与调试"></a>代码编写与调试</h3><p>最常用的场景。Hermes 可以直接操作文件系统，理解项目结构后帮我写代码、修 bug。</p><p>我的 Qx 项目（Tauri 桌面应用，现在 v0.6.5）和 Shelfy 项目（文件整理工具），常见工作流：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">写需求 -&gt; 让 Hermes 分析现有代码 -&gt; 生成代码实现 -&gt; 检查编译 -&gt; 修复错误 -&gt; 提交 PR</span><br></pre></td></tr></table></figure><p>遇到编译错误，直接把错误日志扔给它，它能逐层追溯、定位问题根因。</p><h3 id="代码审查"><a href="#代码审查" class="headerlink" title="代码审查"></a>代码审查</h3><p>PR 提交前用 Hermes 做自动审查——检查安全性、代码质量、风格一致性。发现问题后直接 inline comment 到 GitHub PR 上。</p><h3 id="AI-编码-CLI-集成"><a href="#AI-编码-CLI-集成" class="headerlink" title="AI 编码 CLI 集成"></a>AI 编码 CLI 集成</h3><p>Hermes 不只是自己写代码，还能操作其他 AI 编码工具。我用的 qodercli 就是通过 Hermes 在 tmux 里调度的：</p><ul><li>把任务发给 qodercli</li><li>监控它的输出</li><li>需要时介入修正</li></ul><p>这种”AI 操作 AI”的模式对复杂功能开发特别有效。</p><h2 id="场景二：DevOps-与服务器管理"><a href="#场景二：DevOps-与服务器管理" class="headerlink" title="场景二：DevOps 与服务器管理"></a>场景二：DevOps 与服务器管理</h2><p>我有一台远程 Linux 服务器（frps）和一台飞牛 NAS。Hermes 通过 SSH 直连执行命令：</p><ul><li>配置 frp 内网穿透，开机自启</li><li>检查服务状态、日志</li><li>安装和更新软件包</li><li>管理 Memos 自托管实例</li><li>NAS 文件管理，SMB 挂载排查</li><li>Cloudflare Pages 部署静态站点</li></ul><p>不需要记命令参数，用自然语言描述需求就行。常用的 SSH 模式是 <code>sshpass</code> 加密码直连，避免交互式提示。</p><h2 id="场景三：知识管理与笔记"><a href="#场景三：知识管理与笔记" class="headerlink" title="场景三：知识管理与笔记"></a>场景三：知识管理与笔记</h2><p>我有自托管的 Memos 实例和 Obsidian 笔记库，Hermes 可以直接读写：</p><ul><li>读到有价值的内容，让它记到 Memos</li><li>搜索已有笔记做信息关联</li><li>维护”记忆系统”——跨会话保持用户偏好、环境配置、项目约定</li></ul><p>记忆系统是 Hermes 的特色功能。它会在每次对话时注入之前积累的知识，不用每次重复说”我的博客在哪”、”代理端口是多少”。</p><h2 id="场景四：技能系统与自动化"><a href="#场景四：技能系统与自动化" class="headerlink" title="场景四：技能系统与自动化"></a>场景四：技能系统与自动化</h2><p>Hermes 有”技能”（Skill）机制，预定义特定任务的工作流程。遇到重复性任务就打包成技能，下次直接复用。</p><p>我现在积累的技能包括：</p><ul><li><strong>项目发布</strong>：版本号同步、编译检查、打 tag、推送</li><li><strong>博客维护</strong>：写文章、处理图片、生成部署</li><li><strong>Homebrew Cask 分发</strong>：给 Tauri 应用做 brew tap</li><li><strong>Tauri 发布流程</strong>：多文件版本号更新、CI 验证</li><li><strong>系统诊断</strong>：快速检查磁盘、内存、网络状况</li><li><strong>代码审查</strong>：安全扫描、质量门禁</li></ul><p>把复杂流程封装成技能后，一句话就能启动一整套操作。</p><h2 id="场景五：研究与信息获取"><a href="#场景五：研究与信息获取" class="headerlink" title="场景五：研究与信息获取"></a>场景五：研究与信息获取</h2><p>调研新技术、查 API 文档、找解决方案时：</p><ul><li>搜索网页 + 提取页面内容 + 综合分析</li><li>读 arXiv 论文摘要</li><li>跟踪技术博客更新（blogwatcher）</li><li>跟踪 Polymarket 市场数据</li></ul><p>它能把分散的信息合成一个完整的结论，比自己搜索效率高很多。</p><h2 id="场景六：创意与内容"><a href="#场景六：创意与内容" class="headerlink" title="场景六：创意与内容"></a>场景六：创意与内容</h2><ul><li>用 ComfyUI 生成图片、视频</li><li>生成 ASCII art</li><li>制作 SVG 信息图和架构图</li><li>用 Manim 做数学动画</li><li>TouchDesigner 视觉创意</li></ul><h2 id="场景七：生活工具"><a href="#场景七：生活工具" class="headerlink" title="场景七：生活工具"></a>场景七：生活工具</h2><ul><li><strong>Apple Reminders</strong> – 通过 CLI 管理待办事项</li><li><strong>iMessage</strong> – 发送和接收消息</li><li><strong>地图</strong> – 地理编码、POI 搜索、路线规划</li><li><strong>邮件</strong> – Himalaya CLI 收发邮件</li></ul><h2 id="我的心得"><a href="#我的心得" class="headerlink" title="我的心得"></a>我的心得</h2><p>用了一年，最深的体会是：</p><ol><li><strong>AI 助手的能力 &#x3D; 工具链 x 模型智能</strong>。光有聪明的模型不够，能直接操作系统才是生产力。</li><li><strong>技能系统的价值被低估了</strong>。把重复工作打包成技能，比每次手写 prompt 高效得多。</li><li><strong>记忆系统让 AI 真正”认识”你</strong>。它知道你的项目结构、偏好、常用命令，不用每次从零开始。</li><li><strong>不完美，但在进步</strong>。长上下文仍有 token 耗尽的问题，复杂代码重构有时需要人工介入。但整体趋势是正向的。</li></ol><hr><p><em>这篇文章本身就是 Hermes 帮我写的，从记忆和会话记录中自动整理成文。</em></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>终端代理配置指南</title>
      <link>https://b.xpai.uk/proxy/</link>
      <description>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在中国大陆，GitHub、Homebrew、npm 等开发工具的海外源经常无法直连。最干净的解决方案：本地跑一个代理客户端（Clash Ve]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/proxy/">proxy</category>
      <category domain="https://b.xpai.uk/tags/terminal/">terminal</category>
      <category domain="https://b.xpai.uk/tags/opencode/">opencode</category>
      <category domain="https://b.xpai.uk/tags/codex/">codex</category>
      <category domain="https://b.xpai.uk/tags/brew/">brew</category>
      <category domain="https://b.xpai.uk/tags/github/">github</category>
      <pubDate>Tue, 23 Jun 2026 14:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在中国大陆，GitHub、Homebrew、npm 等开发工具的海外源经常无法直连。最干净的解决方案：本地跑一个代理客户端（Clash Verge &#x2F; ClashX Pro &#x2F; Surge 等），然后在终端里配置环境变量让所有 CLI 工具自动走代理。</p><h2 id="一句话配置"><a href="#一句话配置" class="headerlink" title="一句话配置"></a>一句话配置</h2><p>编辑 <code>~/.zshrc</code>，加入：</p><figure class="highlight zsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 代理</span></span><br><span class="line"><span class="built_in">export</span> http_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line"><span class="built_in">export</span> https_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line"><span class="built_in">export</span> all_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line"><span class="comment"># 不走代理的地址</span></span><br><span class="line"><span class="built_in">export</span> no_proxy=<span class="string">&quot;127.0.0.1,localhost,.local,.localdomain&quot;</span></span><br><span class="line"><span class="built_in">export</span> NO_PROXY=<span class="string">&quot;<span class="variable">$no_proxy</span>&quot;</span></span><br></pre></td></tr></table></figure><p>然后 <code>source ~/.zshrc</code> 即可生效。<strong>7897</strong> 换成你的代理客户端实际端口（Clash 默认 7890&#x2F;7897，Surge 默认 6152 等）。</p><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p>终端的工作流程：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Tabby / iTerm2 / VS Code Terminal</span><br><span class="line">          ↓</span><br><span class="line">     zsh（读取 ~/.zshrc）</span><br><span class="line">          ↓</span><br><span class="line">     http_proxy / https_proxy 环境变量</span><br><span class="line">          ↓</span><br><span class="line">  OpenCode / Codex CLI / brew / curl / git 自动继承</span><br></pre></td></tr></table></figure><p>所有从终端启动的程序都会继承 shell 的环境变量，所以<strong>只需配置一次，所有终端和所有 CLI 工具通用</strong>。</p><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 看看环境变量是否生效</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$http_proxy</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试 curl 能否通过代理访问外网</span></span><br><span class="line">curl -I https://github.com</span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试 brew（之前被墙的）</span></span><br><span class="line">brew tap mcxen/qx</span><br><span class="line">brew install --cask qx</span><br></pre></td></tr></table></figure><h2 id="特殊情况"><a href="#特殊情况" class="headerlink" title="特殊情况"></a>特殊情况</h2><h3 id="brew-tap-被墙-HTTPS-克隆失败"><a href="#brew-tap-被墙-HTTPS-克隆失败" class="headerlink" title="brew tap 被墙 (HTTPS 克隆失败)"></a>brew tap 被墙 (HTTPS 克隆失败)</h3><p>brew tap 默认走 HTTPS 克隆，国内容易被墙。改用 SSH：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git@github.com:mcxen/homebrew-qx.git /opt/homebrew/Library/Taps/mcxen/homebrew-qx</span><br><span class="line">brew trust mcxen/qx</span><br><span class="line">brew install --cask qx</span><br></pre></td></tr></table></figure><h3 id="git-push-超时"><a href="#git-push-超时" class="headerlink" title="git push 超时"></a>git push 超时</h3><p>Github 项目 remote 换成 SSH：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote set-url origin git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><h3 id="VS-Code-Tasks"><a href="#VS-Code-Tasks" class="headerlink" title="VS Code Tasks"></a>VS Code Tasks</h3><p>如果 VS Code 的 Task Runner 不走终端环境变量，在 <code>.vscode/tasks.json</code> 的 task 配置里加 <code>&quot;options&quot;: {&quot;env&quot;: {&quot;http_proxy&quot;: &quot;...&quot;}}</code>。</p><h3 id="no-proxy-的作用"><a href="#no-proxy-的作用" class="headerlink" title="no_proxy 的作用"></a>no_proxy 的作用</h3><p><code>no_proxy</code> 保证内网地址不走代理直连：</p><ul><li>本地开发服务器（<code>localhost:1420</code>、<code>127.0.0.1:3000</code>）</li><li>NAS 内网地址（<code>10.0.0.x</code>）</li><li>局域网路由器（<code>192.168.x.x</code>）</li></ul><p>需要额外添加用逗号分隔：</p><figure class="highlight zsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> no_proxy=<span class="string">&quot;<span class="variable">$no_proxy</span>,10.0.0.0/24,192.168.0.0/16&quot;</span></span><br></pre></td></tr></table></figure><h2 id="推荐实践"><a href="#推荐实践" class="headerlink" title="推荐实践"></a>推荐实践</h2><p>如果要临时开关代理，在 <code>~/.zshrc</code> 加两个函数：</p><figure class="highlight zsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="title">proxy</span></span>() &#123;</span><br><span class="line">  <span class="built_in">export</span> http_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line">  <span class="built_in">export</span> https_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line">  <span class="built_in">export</span> all_proxy=<span class="string">&quot;http://127.0.0.1:7897&quot;</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Proxy ON&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="title">noproxy</span></span>() &#123;</span><br><span class="line">  <span class="built_in">unset</span> http_proxy https_proxy all_proxy</span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Proxy OFF&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用法：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">proxy     <span class="comment"># 开</span></span><br><span class="line">noproxy   <span class="comment"># 关</span></span><br></pre></td></tr></table></figure><h2 id="短链接"><a href="#短链接" class="headerlink" title="短链接"></a>短链接</h2><p>本文永久链接： <strong><a href="https://b.xpai.uk/proxy/">b.xpai.uk&#x2F;proxy</a></strong></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>霞浦半岛 — 沿着东海一号线，从高罗到闾峡</title>
      <link>https://b.xpai.uk/2026/06/20/20260620-xiapu-peninsula/</link>
      <description>
        <![CDATA[<p>接续前一天的霞浦之旅，第二天的路线是沿着 <strong>东海一号线</strong> 深入霞浦半岛，把南线的精华海滩和海湾走了一遍。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      <category domain="https://b.xpai.uk/tags/%E9%9C%9E%E6%B5%A6/">霞浦</category>
      <category domain="https://b.xpai.uk/tags/%E6%91%84%E5%BD%B1/">摄影</category>
      <category domain="https://b.xpai.uk/tags/%E7%A6%8F%E5%BB%BA/">福建</category>
      <pubDate>Sat, 20 Jun 2026 15:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>接续前一天的霞浦之旅，第二天的路线是沿着 <strong>东海一号线</strong> 深入霞浦半岛，把南线的精华海滩和海湾走了一遍。</p><span id="more"></span><h3 id="11-51-·-尾炉坑"><a href="#11-51-·-尾炉坑" class="headerlink" title="11:51 · 尾炉坑"></a>11:51 · 尾炉坑</h3><p>从霞浦城区出来沿东海一号线南下，在长春镇的尾炉坑附近停下来拍了第一张。这条沿海公路是霞浦的景观大道，一边是山一边是海，弯道多、视野开阔。六月的福建已经热起来，但海风吹着还算舒服。</p><p><img src="/images/20260620-xiapu-peninsula/IMG_0912.jpg"></p><h3 id="12-10-·-高罗拾光海岸营地"><a href="#12-10-·-高罗拾光海岸营地" class="headerlink" title="12:10 · 高罗拾光海岸营地"></a>12:10 · 高罗拾光海岸营地</h3><p>到 <strong>高罗海滩</strong> 了。这一带叫 GOL WAVE 高罗拾光海岸营地，是高罗沙滩的一个新开的露营和海滨运动综合体。有水上运动中心，还有一片很开阔的沙滩。沙滩上人不多，海浪平缓，适合下水。</p><p><img src="/images/20260620-xiapu-peninsula/IMG_0914.jpg"></p><h3 id="12-22-·-细仔头岛-界石澳"><a href="#12-22-·-细仔头岛-界石澳" class="headerlink" title="12:22 · 细仔头岛 &#x2F; 界石澳"></a>12:22 · 细仔头岛 &#x2F; 界石澳</h3><p>继续往前到 <strong>细仔头岛</strong> 附近。这里是界石澳的一个小海湾，海水颜色比刚才深了一些，能看到远处岛礁轮廓。路边有几个观景台可以停车拍照。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0919.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0919.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="13-13–13-58-·-大京沙滩"><a href="#13-13–13-58-·-大京沙滩" class="headerlink" title="13:13–13:58 · 大京沙滩"></a>13:13–13:58 · 大京沙滩</h3><p>旅程的重头戏 — <strong>大京沙滩</strong>。这是霞浦最长的沙滩，绵延数公里，沙质细腻、坡度平缓。我们在海鲜城停车场停好车，在沙滩上待了将近一个小时。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0927.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0927.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><p>沙子很细，踩上去很舒服。正午的光线虽然硬，但海水在阳光下的颜色特别好看。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0954.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0954.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><p>沙滩上有不少被海浪冲刷上来的贝壳和小石子。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0964.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0964.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="15-41-·-闾峡村"><a href="#15-41-·-闾峡村" class="headerlink" title="15:41 · 闾峡村"></a>15:41 · 闾峡村</h3><p>离开大京继续南下，到达 <strong>闾峡村</strong>。这里是半岛东南端的一个渔村，附近有闾峡安澜侯宫。村子不大，房子沿着海岸线排开，码头边停着渔船。海水颜色在这一片变得更深更蓝。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0973.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0973.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="16-14-·-里城村-北世"><a href="#16-14-·-里城村-北世" class="headerlink" title="16:14 · 里城村 &#x2F; 北世"></a>16:14 · 里城村 &#x2F; 北世</h3><p>穿过闾峡继续走，经过里城村和北世一带。这一段的东海一号线拐进了内陆，两边是农田和村庄，偶尔能透过树丛看到海。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0977.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0977.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="18-56-·-三沙古桶村"><a href="#18-56-·-三沙古桶村" class="headerlink" title="18:56 · 三沙古桶村"></a>18:56 · 三沙古桶村</h3><p>下午将近七点，到了 <strong>三沙镇古桶村</strong>。三沙是霞浦最热门的摄影点之一，古桶村一带沿海有大量的民宿和海景餐厅，许多都有漂亮的海上日落观景台。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0985.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0985.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="19-04-·-虞公亭沙滩-—-日落"><a href="#19-04-·-虞公亭沙滩-—-日落" class="headerlink" title="19:04 · 虞公亭沙滩 — 日落"></a>19:04 · 虞公亭沙滩 — 日落</h3><p>几分钟车程到 <strong>虞公亭沙滩</strong>，正好赶上日落。六月底的日落时间在七点左右，天空的颜色从金黄过渡到粉紫，映在海面上特别好看。虞公亭沙滩不大，但胜在人少安静。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0989.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0989.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="20-57-·-霞浦城区-—-晚餐"><a href="#20-57-·-霞浦城区-—-晚餐" class="headerlink" title="20:57 · 霞浦城区 — 晚餐"></a>20:57 · 霞浦城区 — 晚餐</h3><p>天黑后回到霞浦城区，在 <strong>松港街道东吾路</strong> 一带找吃的。世纪锦园附近有很多临街的小吃和排挡，和前一晚一样去了新美味园那一带，吃海鲜、喝啤酒，结束了一天的海岸线行程。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0991.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0991.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><h3 id="次日-10-48-·-霞浦东关"><a href="#次日-10-48-·-霞浦东关" class="headerlink" title="次日 10:48 · 霞浦东关"></a>次日 10:48 · 霞浦东关</h3><p>第二天早上在霞浦城区逛了一会儿，东关一带是老城区，街边有菜市场和早点摊，游客少，生活气息很浓。</p><p><img src="/images/20260620-xiapu-peninsula/IMG_0993.jpg"></p><h3 id="12-39-·-福鼎"><a href="#12-39-·-福鼎" class="headerlink" title="12:39 · 福鼎"></a>12:39 · 福鼎</h3><p>返程往北开，中午到了 <strong>福鼎市</strong>。桐城街道洋垱小区一带是居民区，没什么景区，纯粹是路过歇脚。</p><div data-live-photo data-photo-src="/images/20260620-xiapu-peninsula/IMG_0997.jpg" data-video-src="/images/20260620-xiapu-peninsula/IMG_0997.mp4" style="width:100%;max-width:600px;aspect-ratio:4/3;margin:1em auto;"></div><hr><h2 id="实用信息"><a href="#实用信息" class="headerlink" title="实用信息"></a>实用信息</h2><p><strong>路线</strong>：霞浦城区 → 东海一号线南段 → 高罗 → 大京 → 闾峡 → 三沙 → 返程</p><p><strong>东海一号线</strong>：霞浦最美的沿海公路，南起积石、北至三沙，沿途有多个观景台和停车点，海边风景很好，适合自驾慢慢开。</p><p><strong>高罗海滩</strong>：免费开放，有水上运动项目可以玩。</p><p><strong>大京沙滩</strong>：霞浦最长的沙滩，沙质细软，入口处有停车场（海鲜城停车场）。</p><p><strong>闾峡</strong>：半岛东南端的安静渔村，适合拍渔船和海岸线。</p><p><strong>三沙</strong>：摄影圣地，日落时分东壁、虞公亭一带是绝佳的拍摄点。</p><h2 id="照片说明"><a href="#照片说明" class="headerlink" title="照片说明"></a>照片说明</h2><p>本次行程的部分照片包含了 <strong>实况照片 (Live Photo)</strong>——鼠标悬停或手指长按即可看到动态画面。如果看不到动态效果，试试用电脑或手机上的浏览器查看。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>给地图工具加上高德周边POI搜索</title>
      <link>https://b.xpai.uk/2026/06/20/amap-poi-search/</link>
      <description>
        <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>几个月前我给 <code>maps_client.py</code> 接入了高德逆地理编码（<code>reverse</code> 命令），解决了中国地址只能到街道级的痛点。但 <code>nearby</code> 命令（周边POI搜索）一直用的 Overpass API（OpenStreetMap 数据），在中国小县城效果惨不忍睹。</p>
<p>之前在霞浦旅行时搜附近餐厅，霞浦松城 500m 范围内只返回了一家店，还因为 OSM 里 node + way 两条记录重复显示了三次。</p>
<h2 id="方案：高德周边搜索-API"><a href="#方案：高德周边搜索-API" class="headerlink" title="方案：高德周边搜索 API"></a>方案：高德周边搜索 API</h2><p>高德的 <code>place/around</code> 接口（周边POI查询）覆盖了中国全境的餐饮、住宿、医疗、购物等 46 个品类，数据质量远超 OpenStreetMap。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET https://restapi.amap.com/v3/place/around?key=xxx&amp;location=116.3974,39.9042&amp;radius=1000&amp;types=050000&amp;offset=25</span><br></pre></td></tr></table></figure>

<p>返回结构化 JSON，含店名、地址、电话、经纬度、距离、类型等字段。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/Amap/">Amap</category>
      <category domain="https://b.xpai.uk/tags/POI/">POI</category>
      <category domain="https://b.xpai.uk/tags/Maps/">Maps</category>
      <category domain="https://b.xpai.uk/tags/Tooling/">Tooling</category>
      <pubDate>Sat, 20 Jun 2026 04:30:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>几个月前我给 <code>maps_client.py</code> 接入了高德逆地理编码（<code>reverse</code> 命令），解决了中国地址只能到街道级的痛点。但 <code>nearby</code> 命令（周边POI搜索）一直用的 Overpass API（OpenStreetMap 数据），在中国小县城效果惨不忍睹。</p><p>之前在霞浦旅行时搜附近餐厅，霞浦松城 500m 范围内只返回了一家店，还因为 OSM 里 node + way 两条记录重复显示了三次。</p><h2 id="方案：高德周边搜索-API"><a href="#方案：高德周边搜索-API" class="headerlink" title="方案：高德周边搜索 API"></a>方案：高德周边搜索 API</h2><p>高德的 <code>place/around</code> 接口（周边POI查询）覆盖了中国全境的餐饮、住宿、医疗、购物等 46 个品类，数据质量远超 OpenStreetMap。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET https://restapi.amap.com/v3/place/around?key=xxx&amp;location=116.3974,39.9042&amp;radius=1000&amp;types=050000&amp;offset=25</span><br></pre></td></tr></table></figure><p>返回结构化 JSON，含店名、地址、电话、经纬度、距离、类型等字段。</p><span id="more"></span><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><h3 id="1-品类映射"><a href="#1-品类映射" class="headerlink" title="1. 品类映射"></a>1. 品类映射</h3><p>OpenStreetMap 和 高德 使用不同的分类体系。我需要把 <code>maps_client.py</code> 的 46 个品类逐一映射到高德的类型编码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">AMAP_TYPE_MAP = &#123;</span><br><span class="line">    <span class="string">&quot;restaurant&quot;</span>:        <span class="string">&quot;050000&quot;</span>,  <span class="comment"># 餐饮服务</span></span><br><span class="line">    <span class="string">&quot;cafe&quot;</span>:              <span class="string">&quot;050500&quot;</span>,  <span class="comment"># 咖啡厅</span></span><br><span class="line">    <span class="string">&quot;bar&quot;</span>:               <span class="string">&quot;050700&quot;</span>,  <span class="comment"># 酒吧</span></span><br><span class="line">    <span class="string">&quot;hospital&quot;</span>:          <span class="string">&quot;090100&quot;</span>,  <span class="comment"># 综合医院</span></span><br><span class="line">    <span class="string">&quot;hotel&quot;</span>:             <span class="string">&quot;100000&quot;</span>,  <span class="comment"># 住宿服务</span></span><br><span class="line">    <span class="string">&quot;pharmacy&quot;</span>:          <span class="string">&quot;090300&quot;</span>,  <span class="comment"># 药店</span></span><br><span class="line">    <span class="string">&quot;bank&quot;</span>:              <span class="string">&quot;060500&quot;</span>,  <span class="comment"># 银行</span></span><br><span class="line">    <span class="string">&quot;park&quot;</span>:              <span class="string">&quot;110000&quot;</span>,  <span class="comment"># 公园景点</span></span><br><span class="line">    <span class="comment"># ... 共 46 个品类</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-优先策略"><a href="#2-优先策略" class="headerlink" title="2. 优先策略"></a>2. 优先策略</h3><p><code>nearby</code> 命令现在的逻辑是：<strong>先试高德，失败回退 Overpass</strong>。AMAP_KEY 不存在时直接走旧路径，完全向后兼容。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">cmd_nearby</span>(<span class="params">args</span>):</span><br><span class="line">    <span class="comment"># ... 解析参数 ...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 先试高德（中国数据远好于 OSM）</span></span><br><span class="line">    amap_places = amap_nearby(lat, lon, categories, radius, limit)</span><br><span class="line">    <span class="keyword">if</span> amap_places <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> print_json(amap_places)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 回退 Overpass (OSM)</span></span><br><span class="line">    <span class="comment"># ... 原有逻辑 ...</span></span><br></pre></td></tr></table></figure><h3 id="3-配置持久化"><a href="#3-配置持久化" class="headerlink" title="3. 配置持久化"></a>3. 配置持久化</h3><p>Key 现在配置在 Hermes 的 <code>config.yaml</code> 里，脚本启动时自动读取。也可以通过环境变量传入。</p><h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>霞浦松城（26.888, 120.0118）附近 500m 搜餐厅：</p><p><strong>之前（Overpass&#x2F;OSM）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">晨曦酒店 (387m)   ← 酒店被标成了餐厅</span><br></pre></td></tr></table></figure><p><strong>之后（高德）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">互惠小吃店 (106m)</span><br><span class="line">敖祖雄饼店·世纪锦园店 (107m)</span><br><span class="line">长春炒肉糕·粽子·世纪锦园店 (108m)</span><br><span class="line">渔米乡·酸菜鱼·水煮鱼·世纪锦园店 (108m)</span><br><span class="line">新三峡鱼庄·霞浦天安小区店 (112m)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>10 条结果，带地址和电话，距离精确到米。</p><h2 id="配置方式"><a href="#配置方式" class="headerlink" title="配置方式"></a>配置方式</h2><p>如果你也用 <code>maps_client.py</code>，配置高德 Key 很简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hermes config <span class="built_in">set</span> env.AMAP_KEY <span class="string">&quot;你的32位key&quot;</span></span><br></pre></td></tr></table></figure><p>之后 <code>nearby</code> 自动走高德，无需其他配置。没有 Key 时自动走 Overpass 旧路径，不影响海外使用。</p><h2 id="更新内容"><a href="#更新内容" class="headerlink" title="更新内容"></a>更新内容</h2><ul><li><code>maps_client.py</code> v1.4.0<ul><li>新增 <code>amap_nearby()</code> — 高德周边搜索</li><li>46 个品类映射到高德类型编码</li><li>新增 <code>AMAP_TYPE_MAP</code> 常量表</li><li>优先高德，失败回退 Overpass</li><li>支持 <code>AMAP_KEY</code> 从 config.yaml 或环境变量读取</li></ul></li><li><code>maps</code> skill v1.4.0<ul><li>SKILL.md 更新配置说明和 China 覆盖文档</li></ul></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>霞浦之旅 — 从杭州到闽东的九小时海鲜与山海</title>
      <link>https://b.xpai.uk/2026/06/19/20260619-xiapu-trip/</link>
      <description>
        <![CDATA[<h2 id="出发：从杭州到闽东"><a href="#出发：从杭州到闽东" class="headerlink" title="出发：从杭州到闽东"></a>出发：从杭州到闽东</h2><p>六月的一个周六，我们从杭州余杭出发，驱车南下，目标——福建霞浦。</p>
<p><i]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      <category domain="https://b.xpai.uk/tags/%E9%9C%9E%E6%B5%A6/">霞浦</category>
      <category domain="https://b.xpai.uk/tags/%E6%B5%B7%E9%B2%9C/">海鲜</category>
      <category domain="https://b.xpai.uk/tags/%E6%91%84%E5%BD%B1/">摄影</category>
      <pubDate>Fri, 19 Jun 2026 15:59:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="出发：从杭州到闽东"><a href="#出发：从杭州到闽东" class="headerlink" title="出发：从杭州到闽东"></a>出发：从杭州到闽东</h2><p>六月的一个周六，我们从杭州余杭出发，驱车南下，目标——福建霞浦。</p><p><img src="/images/20260619-xiapu/hangzhou-departure.jpg"></p><p>上午九点三十七分，坐进车里，第一眼看到的是仪表盘上那块液晶屏——这是一辆增程式电动车，纯电续航只剩下 20 公里，但燃油续航显示还有 1900 公里。心里踏实了，一脚油门，出发。</p><p>这趟路程全长大约 500 公里，横跨浙江中南部，穿过丽水山区，再翻过浙闽交界的分水关，最终抵达东海之滨的霞浦县城。</p><hr><h2 id="途中：丽水到浙闽交界"><a href="#途中：丽水到浙闽交界" class="headerlink" title="途中：丽水到浙闽交界"></a>途中：丽水到浙闽交界</h2><p>开了四个小时，下午一点五十五分，我们在丽水太平乡附近的一个服务区停靠。休息区里挂着一幅《浙江省干线公路图》，我们凑上去看路线——还有一大半路程要走，身在丽水，心已飞向大海。</p><p><img src="/images/20260619-xiapu/lishui-map.jpg"></p><p>继续南下，下午五点终于到达浙闽交界的苍南县炎亭镇。这里是浙江的最南端，再往前走一步就是福建了。</p><p><img src="/images/20260619-xiapu/yanjing-mazu-temple.jpg"></p><p>炎亭沿海有一座妈祖庙，气势恢宏。朱红墙体、金色匾额、盘龙石柱、重檐歇山顶——典型的浙闽沿海庙宇建筑。妈祖是沿海渔民的海上守护神，每到一处海滨，看到妈祖庙就知道离海不远了。</p><p>从妈祖庙往海边走几步，一幅壮阔的海岸画卷在眼前展开。</p><p><img src="/images/20260619-xiapu/yanjing-coast.jpg"></p><p>傍晚五点的光线透过云层洒在海面上，左侧是灰黄色的近海，布满细密的波纹；右侧是陡峭的海蚀崖壁，黄褐色与深灰色的岩石裸露着。几簇常绿树冠从边缘伸入画面，像是天然的画框。这里没有游客，没有渔船，只有风声、海浪和一片无人打扰的荒野海岸。</p><hr><h2 id="抵达霞浦"><a href="#抵达霞浦" class="headerlink" title="抵达霞浦"></a>抵达霞浦</h2><p>翻过浙闽交界的分水关，驶入福建境内，沿途的山海景色越发迷人。傍晚六点，我们跨过了霞浦的界碑。</p><p>晚上七点整，我们驶入霞浦城区。这座闽东小城正沐浴在暮色之中。</p><p><img src="/images/20260619-xiapu/xiapu-evening.jpg"></p><p>从车窗望出去，霞浦的晚高峰车流不算拥挤，但也络绎不绝。一块蓝色路牌标注着通往各个景区的方向——罗汉溪景区、大京景区、长春镇……对于一个第一次来的人，每一块路牌都在发出邀请。远处的建筑上挂着红色灯笼，LED 广告牌已经亮起，一座形似船帆的地标在暮色中若隐若现。</p><p><strong>霞浦</strong>，坐落于福建省东北部海岸线，隶属宁德市，坐拥 505 公里海岸线——是福建海岸线最长的县份。这里以”中国最美滩涂”之名享誉摄影圈，北岐的日出、小皓的虎皮沙滩、东壁的日落……每年吸引着无数摄影爱好者前来朝圣。四至六月是海带丰收季，九至十一月则迎来紫菜种植季和最佳光线，是霞浦最迷人的时节。</p><hr><h2 id="海鲜之夜"><a href="#海鲜之夜" class="headerlink" title="海鲜之夜"></a>海鲜之夜</h2><p>在酒店安顿好行李，肚子早已咕咕叫。霞浦靠海吃海，来到这儿不吃一顿地道的海鲜，等于白来。</p><p>我们直奔霞浦城区——松港街道东吾路·世纪锦园一带，在<strong>新美味园</strong>落座。这家店虽不大，但烟火气十足，正是本地人常去的那种馆子。</p><p><strong>第一轮：晚餐</strong></p><p><img src="/images/20260619-xiapu/xiapu-dinner1.jpg"></p><p>二十点二十一分，第一桌菜上来了。一盘辣炒碎螺，蒜末葱花辣椒段浸润在橙黄油亮的汤汁里，螺肉细小但鲜味十足；一盘葱爆海蛎，白色的蛎肉饱满肥嫩，配着翠绿的葱段和姜片，汤汁金黄透亮。桌角的一只玻璃杯上印着”霞浦”二字——不需要更多的招牌了，光这两道菜就足以说明这座小城的味道。</p><p><strong>第二轮：夜宵加码</strong></p><p><img src="/images/20260619-xiapu/xiapu-dinner2.jpg"></p><p>同样是二十点二十一分（可能是在同一家店连续加菜），桌子已经摆不下了。一盘爆炒海瓜子，混合着葱花蒜末红辣椒，葱油的香气扑鼻而来；后方还有一份蒜蓉鲍鱼，右下角的骨碟里堆满了贝壳残骸。桌上散落着空碗、筷子和一个绿色的喜力啤酒开瓶器。这才是霞浦夜宵的正确打开方式——不在乎环境，只在乎味道。</p><p><strong>第三轮：大排档深度体验</strong></p><p><img src="/images/20260619-xiapu/xiapu-night-market.jpg"></p><p>二十点四十分，我们换了一个阵地——或者说是被隔壁的大排档吸引了过去。竹笼蒸煮的生蚝和蛤蜊冒着热气，螺类与海瓜子堆满了盘子，还有一盘晶莹剔透的鱼皮冻或凉拌海蜇。穿着深蓝色围裙的服务员端着菜在拥挤的通道里穿梭，旁边桌的食客们聊得热火朝天。</p><p>霞浦的海鲜之所以出名，是因为这里是中国最大的海带和紫菜产区之一，同时也是”中国大黄鱼之乡”。这里的每一道菜都带着海水的咸鲜味，不需要复杂的烹饪技法——清蒸、白灼、葱油，寥寥几笔，便是最好的呈现。</p><hr><h2 id="夜游松城"><a href="#夜游松城" class="headerlink" title="夜游松城"></a>夜游松城</h2><p>饱餐一顿后在街上散步，霞浦的夜生活才刚刚开始。</p><p><img src="/images/20260619-xiapu/xiapu-night-street.jpg"></p><p>晚上十点过后，街上的行人和车辆已经不多，整座小城逐渐安静下来。路灯把柏油路面照得温润发亮，街道两旁的电线杆和老旧楼房在昏黄的灯光下投出长长的影子。对于一个疲惫的旅人来说，这样的夜晚恰到好处——晚风带着海的气息轻轻吹过，肚子里装满了海鲜，心里是对明天滩涂日出的期待。</p><hr><h2 id="实用信息"><a href="#实用信息" class="headerlink" title="实用信息"></a>实用信息</h2><ul><li><strong>杭州 → 霞浦</strong>：自驾约 500 公里，需时 5-6 小时，经 G25 长深高速转 G15 沈海高速</li><li><strong>途经点推荐</strong>：丽水服务区看浙南山景、苍南炎亭妈祖庙（浙闽交界标志）</li><li><strong>最佳季节</strong>：4-6 月（海带丰收季）和 9-11 月（紫菜季，光线通透）</li><li><strong>必吃海鲜</strong>：碎螺、海蛎、海瓜子、蒜蓉鲍鱼、蒸生蚝</li><li><strong>推荐餐厅</strong>：新美味园（东吾路世纪锦园附近）</li><li><strong>美食集中地</strong>：霞浦松港街道东吾路·世纪锦园一带</li><li><strong>摄影推荐</strong>：北岐日出、小皓虎皮沙滩、东壁日落、杨家溪古榕</li><li><strong>住宿</strong>：建议住松城城区，出行方便</li></ul><hr><p><em>拍摄设备：iPhone 17 Pro | 部分照片由 IDG 记录仪拍摄</em><br><em>路线：杭州余杭 → 丽水太平乡 → 苍南炎亭（浙闽交界）→ 霞浦松城</em></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>用 Cloudflare Workers 搭建战术终端风格界面</title>
      <link>https://b.xpai.uk/2026/06/17/cloudflare-workers-tactical-terminal/</link>
      <description>
        <![CDATA[<p>最近给 GeekZip 的下载页做了个「战术终端」风格的界面，跑了 Cloudflare Workers 上。记录一下思路和实现。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/categories/%E5%BC%80%E5%8F%91/">开发</category>
      <category domain="https://b.xpai.uk/tags/Cloudflare/">Cloudflare</category>
      <category domain="https://b.xpai.uk/tags/Workers/">Workers</category>
      <category domain="https://b.xpai.uk/tags/CSS/">CSS</category>
      <category domain="https://b.xpai.uk/tags/%E5%89%8D%E7%AB%AF/">前端</category>
      <pubDate>Wed, 17 Jun 2026 12:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近给 GeekZip 的下载页做了个「战术终端」风格的界面，跑了 Cloudflare Workers 上。记录一下思路和实现。</p><span id="more"></span><h2 id="为什么选-Workers"><a href="#为什么选-Workers" class="headerlink" title="为什么选 Workers"></a>为什么选 Workers</h2><p>简单原则：<strong>不要多一个服务器</strong>。GeekZip 本身在 GitHub Releases 上托管二进制，但需要一个好看的下载页。如果为了一个静态页面去配 Nginx&#x2F;Caddy&#x2F;Vercel，太重了。</p><p>Workers 的好处：</p><ul><li>免费计划每天 10 万请求，个人项目用不完</li><li>全球边缘节点，延迟约等于 0</li><li>可以在 fetch handler 里直接代理 GitHub 下载，比静态 CDN 灵活</li><li>自定义域名绑定一行命令搞定</li></ul><h2 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">用户 → z.xpai.uk</span><br><span class="line">         ↓</span><br><span class="line">    Cloudflare Edge</span><br><span class="line">         ↓</span><br><span class="line">    Worker (geekzip)</span><br><span class="line">         ├── /  → 返回战术终端 HTML</span><br><span class="line">         ├── /download/*  → 代理 GitHub Release 文件</span><br><span class="line">         └── /api/releases → 转发 GitHub API</span><br></pre></td></tr></table></figure><p>Worker 的代码结构很简单——一个 <code>fetch()</code> 函数根据路径分发：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">fetch</span>(<span class="params">request</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(request.<span class="property">url</span>);</span><br><span class="line">    <span class="keyword">if</span> (url.<span class="property">pathname</span> === <span class="string">&quot;/&quot;</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="variable constant_">HTML</span>, &#123;</span><br><span class="line">        <span class="attr">headers</span>: &#123; <span class="string">&quot;content-type&quot;</span>: <span class="string">&quot;text/html;charset=UTF-8&quot;</span> &#125;</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (url.<span class="property">pathname</span>.<span class="title function_">startsWith</span>(<span class="string">&quot;/download/&quot;</span>)) &#123;</span><br><span class="line">      <span class="comment">// 代理下载</span></span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="战术终端设计"><a href="#战术终端设计" class="headerlink" title="战术终端设计"></a>战术终端设计</h2><p>风格参考了军用终端 UI 和 CRT 显示器的视觉效果。全部用纯 CSS 实现，没有图片。</p><h3 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h3><p>正文用 <strong>JetBrains Mono</strong>（等宽字体，编程感），标题用 <strong>Major Mono Display</strong>（粗犷的显示字体，军事感）。Google Fonts 加载：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">href</span>=<span class="string">&quot;https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&amp;family=Major+Mono+Display&amp;display=swap&quot;</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="单色调色盘"><a href="#单色调色盘" class="headerlink" title="单色调色盘"></a>单色调色盘</h3><p>只用了绿色 <code>#00ff95</code>，通过透明度变化分出层次：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-pseudo">:root</span> &#123;</span><br><span class="line">  <span class="attr">--green</span>: <span class="number">#00ff95</span>;</span><br><span class="line">  <span class="attr">--green-dim</span>: <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">255</span>,<span class="number">149</span>,<span class="number">0.15</span>);</span><br><span class="line">  <span class="attr">--green-mid</span>: <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">255</span>,<span class="number">149</span>,<span class="number">0.5</span>);</span><br><span class="line">  <span class="attr">--border</span>: <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">255</span>,<span class="number">149</span>,<span class="number">0.2</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="CRT-扫描线"><a href="#CRT-扫描线" class="headerlink" title="CRT 扫描线"></a>CRT 扫描线</h3><p>用 <code>repeating-linear-gradient</code> 在 <code>body::after</code> 上绘制 2px 间隔的黑线，模拟老式 CRT 显示器的扫描线纹理：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">body</span><span class="selector-pseudo">::after</span> &#123;</span><br><span class="line">  <span class="attribute">content</span>: <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">  <span class="attribute">position</span>: fixed;</span><br><span class="line">  <span class="attribute">inset</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">background</span>: <span class="built_in">repeating-linear-gradient</span>(</span><br><span class="line">    <span class="number">0deg</span>,</span><br><span class="line">    transparent,</span><br><span class="line">    transparent <span class="number">2px</span>,</span><br><span class="line">    <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.08</span>) <span class="number">2px</span>,</span><br><span class="line">    <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.08</span>) <span class="number">4px</span></span><br><span class="line">  );</span><br><span class="line">  <span class="attribute">pointer-events</span>: none;</span><br><span class="line">  <span class="attribute">z-index</span>: <span class="number">999</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再加一个径向渐变模拟 CRT 屏幕边缘的暗角：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">body</span><span class="selector-pseudo">::before</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: <span class="built_in">radial-gradient</span>(</span><br><span class="line">    ellipse at center,</span><br><span class="line">    transparent <span class="number">60%</span>,</span><br><span class="line">    <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.4</span>) <span class="number">100%</span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="四角作战标记"><a href="#四角作战标记" class="headerlink" title="四角作战标记"></a>四角作战标记</h3><p>四个 <code>&lt;svg&gt;</code> 定位在页面的四个角，L 形线条，旋转&#x2F;镜像组合：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;corner corner-tl&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">svg</span> <span class="attr">viewBox</span>=<span class="string">&quot;0 0 28 28&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">path</span> <span class="attr">d</span>=<span class="string">&quot;M26,2 L26,10 M26,2 L18,2 M26,2 L2,26&quot;</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>四个的变换分别是 <code>scaleX(-1)</code>、<code>scaleY(-1)</code>、<code>scale(-1,-1)</code>，形成一个封闭的”框”效果。</p><h3 id="LED-脉冲指示灯"><a href="#LED-脉冲指示灯" class="headerlink" title="LED 脉冲指示灯"></a>LED 脉冲指示灯</h3><p>用 CSS animation 模拟三个状态指示灯（SYS、NET、DOCK），<code>pulse</code> 关键帧控制发光圆点的呼吸节奏：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.led-dot</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">6px</span>; <span class="attribute">height</span>: <span class="number">6px</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="built_in">var</span>(--green);</span><br><span class="line">  <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">0</span> <span class="number">4px</span> <span class="built_in">var</span>(--green);</span><br><span class="line">  <span class="attribute">animation</span>: pulse <span class="number">2s</span> ease-in-out infinite;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">@keyframes</span> pulse &#123;</span><br><span class="line">  <span class="number">0%</span>, <span class="number">100%</span> &#123; <span class="attribute">opacity</span>: <span class="number">1</span>; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1</span>); &#125;</span><br><span class="line">  <span class="number">50%</span> &#123; <span class="attribute">opacity</span>: <span class="number">0.3</span>; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">0.8</span>); &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>DOCK 指示灯用 <code>.off</code> 类关掉（灰色），表示「当前未对接」。</p><h3 id="按钮扫光动画"><a href="#按钮扫光动画" class="headerlink" title="按钮扫光动画"></a>按钮扫光动画</h3><p>鼠标移入下载按钮时，一道高光从左侧扫过。用伪元素 <code>::before</code> 实现：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.dl-btn</span><span class="selector-pseudo">::before</span> &#123;</span><br><span class="line">  <span class="attribute">content</span>: <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  <span class="attribute">top</span>: <span class="number">0</span>; <span class="attribute">left</span>: -<span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">100%</span>; <span class="attribute">height</span>: <span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">background</span>: <span class="built_in">linear-gradient</span>(</span><br><span class="line">    <span class="number">90deg</span>,</span><br><span class="line">    transparent,</span><br><span class="line">    <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">255</span>,<span class="number">149</span>,<span class="number">0.08</span>),</span><br><span class="line">    transparent</span><br><span class="line">  );</span><br><span class="line">  <span class="attribute">transition</span>: left <span class="number">0.5s</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="selector-class">.dl-btn</span><span class="selector-pseudo">:hover</span><span class="selector-pseudo">::before</span> &#123;</span><br><span class="line">  <span class="attribute">left</span>: <span class="number">100%</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="CLI-命令框-闪烁光标"><a href="#CLI-命令框-闪烁光标" class="headerlink" title="CLI 命令框 + 闪烁光标"></a>CLI 命令框 + 闪烁光标</h3><p>模拟终端输入效果，<code>cursor</code> 用 step-end 动画实现 blink：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.cli-box</span> <span class="selector-class">.cursor</span> &#123;</span><br><span class="line">  <span class="attribute">display</span>: inline-block;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">7px</span>; <span class="attribute">height</span>: <span class="number">14px</span>;</span><br><span class="line">  <span class="attribute">background</span>: <span class="built_in">var</span>(--green);</span><br><span class="line">  <span class="attribute">animation</span>: blink <span class="number">1s</span> step-end infinite;</span><br><span class="line">  <span class="attribute">vertical-align</span>: text-bottom;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">@keyframes</span> blink &#123; <span class="number">50%</span> &#123; <span class="attribute">opacity</span>: <span class="number">0</span>; &#125; &#125;</span><br></pre></td></tr></table></figure><h3 id="操作日志"><a href="#操作日志" class="headerlink" title="操作日志"></a>操作日志</h3><p>日志区域模拟终端输出，每行包含时间戳、等级标签和消息。等级用颜色区分：</p><ul><li>INFO → 半透明白</li><li>OK → 亮绿</li><li>WARN → 橙黄</li><li>ERR → 红色</li></ul><p>偶数列用 <code>nth-child(even)</code> 加微弱斑马纹，类似 TUI 日志查看器。</p><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>使用 <code>wrangler</code> CLI，一条命令搞定：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 wrangler</span></span><br><span class="line">npm install -g wrangler</span><br><span class="line"></span><br><span class="line"><span class="comment"># 登录（或用 API token 环境变量）</span></span><br><span class="line"><span class="built_in">export</span> CLOUDFLARE_API_TOKEN=<span class="string">&quot;cfat_xxxx&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 部署并绑定自定义域名</span></span><br><span class="line">wrangler deploy --domain z.xpai.uk</span><br></pre></td></tr></table></figure><p>Worker 的路由配置在 <code>wrangler.toml</code>：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name</span> = <span class="string">&quot;geekzip&quot;</span></span><br><span class="line"><span class="attr">main</span> = <span class="string">&quot;index.js&quot;</span></span><br><span class="line"><span class="attr">compatibility_date</span> = <span class="string">&quot;2026-06-17&quot;</span></span><br></pre></td></tr></table></figure><h2 id="下载代理"><a href="#下载代理" class="headerlink" title="下载代理"></a>下载代理</h2><p>Worker 同时担任 GitHub Release 的下载代理。<code>/download/GeekZip-macos-arm64.app.zip</code> 会被转发到 <code>https://github.com/mcxen/GeekZip/releases/latest/download/...</code>，这样用户下载时走 Cloudflare 边缘节点，比直连 GitHub 快很多。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (url.<span class="property">pathname</span>.<span class="title function_">startsWith</span>(<span class="string">&quot;/download/&quot;</span>)) &#123;</span><br><span class="line">  <span class="keyword">const</span> asset = url.<span class="property">pathname</span>.<span class="title function_">slice</span>(<span class="number">10</span>);</span><br><span class="line">  <span class="keyword">const</span> gh = <span class="keyword">await</span> <span class="title function_">fetch</span>(</span><br><span class="line">    <span class="string">&quot;https://github.com/mcxen/GeekZip/releases/latest/download/&quot;</span> + asset,</span><br><span class="line">    &#123; <span class="attr">headers</span>: &#123; <span class="string">&quot;User-Agent&quot;</span>: <span class="string">&quot;GeekZip-Worker&quot;</span> &#125; &#125;</span><br><span class="line">  );</span><br><span class="line">  <span class="keyword">const</span> hdrs = <span class="keyword">new</span> <span class="title class_">Headers</span>(gh.<span class="property">headers</span>);</span><br><span class="line">  hdrs.<span class="title function_">set</span>(<span class="string">&quot;content-disposition&quot;</span>, <span class="string">&#x27;attachment; filename=&quot;&#x27;</span> + asset + <span class="string">&#x27;&quot;&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(gh.<span class="property">body</span>, &#123; <span class="attr">headers</span>: hdrs &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="踩坑记录"><a href="#踩坑记录" class="headerlink" title="踩坑记录"></a>踩坑记录</h2><h3 id="DNS-的问题"><a href="#DNS-的问题" class="headerlink" title="DNS 的问题"></a>DNS 的问题</h3><p>一开始把 <code>go.opencode.ai</code> 和 <code>opencode.ai/zen/go/v1</code> 搞混了，在 DNS 上浪费了不少时间。检查域名是否真的是公共 DNS 记录，可以直接 <code>dig +short &lt;domain&gt;</code> 验证。</p><h3 id="透明代理-vs-显式代理"><a href="#透明代理-vs-显式代理" class="headerlink" title="透明代理 vs 显式代理"></a>透明代理 vs 显式代理</h3><p>在 Ubuntu VM上试验时，iptables REDIRECT 把 443 流量导到代理的 redir-port，但代理看不到原始域名（只有 IP），导致路由失败。显式 HTTP CONNECT 代理更可靠。</p><h3 id="Workers-自定义域名"><a href="#Workers-自定义域名" class="headerlink" title="Workers 自定义域名"></a>Workers 自定义域名</h3><p><code>wrangler deploy --domain</code> 会自动处理 DNS 记录和 Workers Route 的绑定，不需要手动去 Cloudflare Dashboard 点。但需要注意 API Token 需要有 Workers 和 DNS 的权限。</p><h2 id="最终效果"><a href="#最终效果" class="headerlink" title="最终效果"></a>最终效果</h2><p>访问 <a href="https://z.xpai.uk/"><code>https://z.xpai.uk</code></a> 可以看到成品。整个页面不到 12KB（gzip 后 3.6KB），没有依赖任何框架或 UI 库，纯手写 CSS + Workers。代码在 <a href="https://github.com/mcxen/GeekZip">GeekZip 仓库</a> 的 <code>workers/</code> 目录下。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>给地图工具加上高德逆地理编码</title>
      <link>https://b.xpai.uk/2026/06/14/amap-reverse-geocoding/</link>
      <description>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>我日常用的地图工具（<code>maps_client.py</code>）基于 OpenStreetMap 生态 —— Nominatim 做 geocode&#x2F;reverse geocode，OSRM 算路径，Overpass 查 POI。这套组合在全球大部分地区够用，但在中国有个硬伤：</p>
<p><strong>Nominatim 的中国地址数据极其稀疏。</strong> 输入一组经纬度，返回的往往只有街道名（如”玉清东街”），没有小区名、没有楼栋、没有 landmark。对于 “我在哪个小区附近” 这种日常需求，完全不可用。</p>
<h2 id="方案：接入高德-Web-服务-API"><a href="#方案：接入高德-Web-服务-API" class="headerlink" title="方案：接入高德 Web 服务 API"></a>方案：接入高德 Web 服务 API</h2><p>高德开放平台提供 Web 服务 API，免费个人开发者每天 5000 次调用额度。<code>regeo</code> 接口（逆地理编码）直接返回：</p>
<ul>
<li>格式化地址（省市区街道+地标）</li>
<li>小区&#x2F;建筑物名称</li>
<li>周边 POI 列表（医院、学校、商场等）</li>
</ul>
<p>接入成本极低：一个 HTTP GET 请求。</p>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/Amap/">Amap</category>
      <category domain="https://b.xpai.uk/tags/Maps/">Maps</category>
      <category domain="https://b.xpai.uk/tags/Tooling/">Tooling</category>
      <category domain="https://b.xpai.uk/tags/Geocoding/">Geocoding</category>
      <pubDate>Sun, 14 Jun 2026 14:30:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>我日常用的地图工具（<code>maps_client.py</code>）基于 OpenStreetMap 生态 —— Nominatim 做 geocode&#x2F;reverse geocode，OSRM 算路径，Overpass 查 POI。这套组合在全球大部分地区够用，但在中国有个硬伤：</p><p><strong>Nominatim 的中国地址数据极其稀疏。</strong> 输入一组经纬度，返回的往往只有街道名（如”玉清东街”），没有小区名、没有楼栋、没有 landmark。对于 “我在哪个小区附近” 这种日常需求，完全不可用。</p><h2 id="方案：接入高德-Web-服务-API"><a href="#方案：接入高德-Web-服务-API" class="headerlink" title="方案：接入高德 Web 服务 API"></a>方案：接入高德 Web 服务 API</h2><p>高德开放平台提供 Web 服务 API，免费个人开发者每天 5000 次调用额度。<code>regeo</code> 接口（逆地理编码）直接返回：</p><ul><li>格式化地址（省市区街道+地标）</li><li>小区&#x2F;建筑物名称</li><li>周边 POI 列表（医院、学校、商场等）</li></ul><p>接入成本极低：一个 HTTP GET 请求。</p><span id="more"></span><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><h3 id="1-申请-Key"><a href="#1-申请-Key" class="headerlink" title="1. 申请 Key"></a>1. 申请 Key</h3><p>去 <a href="https://console.amap.com/">高德开放平台控制台</a> 注册个人开发者 → 创建应用 → 添加 <strong>Web 服务 API</strong> 类型的 Key。不需要绑域名，不需要备案。</p><p>Key 是一串 32 位 hex 字符串，像这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">5bcc282ebffd771255da0aca935b6636</span><br></pre></td></tr></table></figure><h3 id="2-API-调用"><a href="#2-API-调用" class="headerlink" title="2. API 调用"></a>2. API 调用</h3><p>高德的 regeo 接口极其简单：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET https://restapi.amap.com/v3/geocode/regeo?key=YOUR_KEY&amp;location=116.3974,39.9042&amp;output=json&amp;extensions=all</span><br></pre></td></tr></table></figure><p>返回示例（北京天安门）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;regeocode&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;formatted_address&quot;</span><span class="punctuation">:</span> <span class="string">&quot;北京市东城区东华门街道天安门广场人民英雄纪念碑&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;addressComponent&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;province&quot;</span><span class="punctuation">:</span> <span class="string">&quot;北京市&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;district&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东城区&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;township&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东华门街道&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;building&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;天安门广场&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;风景名胜;公园广场;城市广场&quot;</span><span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;pois&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;人民英雄纪念碑&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;风景名胜;纪念馆&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;distance&quot;</span><span class="punctuation">:</span> <span class="string">&quot;54&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;天安门广场&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;风景名胜;公园广场&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;distance&quot;</span><span class="punctuation">:</span> <span class="string">&quot;117&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;毛主席纪念堂&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;风景名胜;纪念馆&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;distance&quot;</span><span class="punctuation">:</span> <span class="string">&quot;190&quot;</span><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">&#125;</span></span><br></pre></td></tr></table></figure><p>对比 Nominatim 同一位置的返回：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;display_name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东长安街, 南池子社区, 东华门街道, 东城区, 北京市, 100010, 中国&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;address&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="attr">&quot;road&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东长安街&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;suburb&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东华门街道&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;city&quot;</span><span class="punctuation">:</span> <span class="string">&quot;东城区&quot;</span><span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>差距明显</strong>：Nominatim 只给到街道级，高德给到”人民英雄纪念碑” + 周边 5 个 POI。</p><h3 id="3-代码改动"><a href="#3-代码改动" class="headerlink" title="3. 代码改动"></a>3. 代码改动</h3><p>在 <code>maps_client.py</code> 的 <code>cmd_reverse</code> 函数里，优先尝试 Amap，失败则回退 Nominatim：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">AMAP_REVERSE = <span class="string">&quot;https://restapi.amap.com/v3/geocode/regeo&quot;</span></span><br><span class="line">AMAP_KEY = os.environ.get(<span class="string">&quot;AMAP_KEY&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">cmd_reverse</span>(<span class="params">args</span>):</span><br><span class="line">    <span class="comment"># ... 参数校验 ...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 先试高德</span></span><br><span class="line">    <span class="keyword">if</span> AMAP_KEY:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            params = &#123;</span><br><span class="line">                <span class="string">&quot;key&quot;</span>: AMAP_KEY,</span><br><span class="line">                <span class="string">&quot;location&quot;</span>: <span class="string">f&quot;<span class="subst">&#123;lon&#125;</span>,<span class="subst">&#123;lat&#125;</span>&quot;</span>,</span><br><span class="line">                <span class="string">&quot;output&quot;</span>: <span class="string">&quot;json&quot;</span>,</span><br><span class="line">                <span class="string">&quot;extensions&quot;</span>: <span class="string">&quot;all&quot;</span>,</span><br><span class="line">                <span class="string">&quot;radius&quot;</span>: <span class="number">1000</span>,</span><br><span class="line">            &#125;</span><br><span class="line">            data = http_get(AMAP_REVERSE, params=params, silent=<span class="literal">True</span>)</span><br><span class="line">            <span class="keyword">if</span> data.get(<span class="string">&quot;status&quot;</span>) == <span class="string">&quot;1&quot;</span> <span class="keyword">and</span> data.get(<span class="string">&quot;regeocode&quot;</span>):</span><br><span class="line">                regeo = data[<span class="string">&quot;regeocode&quot;</span>]</span><br><span class="line">                <span class="comment"># 提取小区名</span></span><br><span class="line">                <span class="keyword">for</span> poi <span class="keyword">in</span> regeo.get(<span class="string">&quot;pois&quot;</span>, [])[:<span class="number">10</span>]:</span><br><span class="line">                    <span class="keyword">if</span> <span class="string">&quot;住宅小区&quot;</span> <span class="keyword">in</span> poi.get(<span class="string">&quot;type&quot;</span>, <span class="string">&quot;&quot;</span>):</span><br><span class="line">                        community = poi[<span class="string">&quot;name&quot;</span>]</span><br><span class="line">                <span class="comment"># 输出结构化结果</span></span><br><span class="line">                <span class="keyword">return</span> print_json(&#123;...&#125;)</span><br><span class="line">        <span class="keyword">except</span>:</span><br><span class="line">            <span class="keyword">pass</span>  <span class="comment"># 失败则 fall through</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 回退 Nominatim</span></span><br><span class="line">    <span class="keyword">return</span> nominatim_reverse(lat, lon)</span><br></pre></td></tr></table></figure><p>完整改动在 <code>maps_client.py v1.3.0</code>。Python stdlib 即可，零外部依赖。</p><h3 id="4-效果对比"><a href="#4-效果对比" class="headerlink" title="4. 效果对比"></a>4. 效果对比</h3><p>青岛市区某点（36.07, 120.38）：</p><p><strong>Nominatim（之前）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">仅返回街道名，无小区信息</span><br></pre></td></tr></table></figure><p><strong>Amap&#x2F;高德地图（之后）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">山东省青岛市市南区香港中路街道华佗路中国人民解放军海军第九七一医院 · 在正一家园附近</span><br><span class="line"></span><br><span class="line">周边 POI:</span><br><span class="line">- 中国人民解放军海军第九七一医院 (226m)</span><br><span class="line">- 闽江路52号小区 (87m)</span><br><span class="line">- 正一家园 (126m) ← 小区级别</span><br><span class="line">- 青岛市教育局 (177m)</span><br><span class="line">- 府新大厦 (179m)</span><br></pre></td></tr></table></figure><p>“在正一家园附近” —— 这才是日常问路需要的精度。</p><h2 id="配置方式"><a href="#配置方式" class="headerlink" title="配置方式"></a>配置方式</h2><p>设置环境变量即可启用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> AMAP_KEY=<span class="string">&quot;你的32位key&quot;</span></span><br></pre></td></tr></table></figure><p>也可以写进 shell profile：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;export AMAP_KEY=&quot;你的32位key&quot;&#x27;</span> &gt;&gt; ~/.zshrc</span><br></pre></td></tr></table></figure><p>之后 <code>maps_client.py reverse 纬度 经度</code> 自动走高德，无需任何其他配置。</p><h2 id="踩坑记录"><a href="#踩坑记录" class="headerlink" title="踩坑记录"></a>踩坑记录</h2><h3 id="1-Key-类型必须选-Web-服务-API"><a href="#1-Key-类型必须选-Web-服务-API" class="headerlink" title="1. Key 类型必须选 Web 服务 API"></a>1. Key 类型必须选 Web 服务 API</h3><p>高德有多个产品线：JS API、Android SDK、iOS SDK、Web 服务 API。如果选了 JS API 或 SDK 类型的 key，调用 REST 接口会返回：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;info&quot;</span><span class="punctuation">:</span> <span class="string">&quot;USERKEY_PLAT_NOMATCH&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;infocode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;10009&quot;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>解决方法</strong>：在控制台重新创建一个”Web 服务 API”类型的 key。</p><h3 id="2-免费的够用"><a href="#2-免费的够用" class="headerlink" title="2. 免费的够用"></a>2. 免费的够用</h3><p>个人开发者免费额度 5000 次&#x2F;天。对于个人工具使用，完全不可能用完 —— 一天手动查几十次已经很多了。</p><h3 id="3-非中国坐标自动回退"><a href="#3-非中国坐标自动回退" class="headerlink" title="3. 非中国坐标自动回退"></a>3. 非中国坐标自动回退</h3><p>高德只覆盖中国地区。代码里做了 fallback：如果高德请求失败（网络超时、非中国坐标等），自动降级到 Nominatim，不影响海外使用。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li><strong>问题</strong>：Nominatim 中国数据稀疏，只能到街道级</li><li><strong>方案</strong>：优先调用高德 Web 服务 API，失败回退 Nominatim</li><li><strong>成效</strong>：小区级地址 + 周边 POI，零外部依赖</li><li><strong>成本</strong>：免费（5000次&#x2F;天），一行环境变量即可启用</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hermes Agent DeepSeek API 认证失败排查与修复</title>
      <link>https://b.xpai.uk/2026/06/12/hermes-deepseek-auth-fix/</link>
      <description>
        <![CDATA[<p><img src="/hermes-deepseek-auth-fix.assets/theo-hall-pmUgUMvfmUg-unsplash.jpg" alt="theo-hall-pmUgUMvfmUg-unsplash"></p>
<h2 id="背景"><a h]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/Hermes/">Hermes</category>
      <category domain="https://b.xpai.uk/tags/DeepSeek/">DeepSeek</category>
      <category domain="https://b.xpai.uk/tags/Troubleshooting/">Troubleshooting</category>
      <category domain="https://b.xpai.uk/tags/DevOps/">DevOps</category>
      <pubDate>Thu, 11 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><img src="/hermes-deepseek-auth-fix.assets/theo-hall-pmUgUMvfmUg-unsplash.jpg" alt="theo-hall-pmUgUMvfmUg-unsplash"></p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最近在使用 Hermes Agent 时，所有请求突然全部失败，Gateway 日志反复报出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error code: 401 - Authentication Fails, Your api key: ****OcWp is invalid</span><br></pre></td></tr></table></figure><p>伴随 <code>Transient agent failure</code> 错误。API key 明明刚换过新的，为什么一直 401？</p><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><ul><li>Hermes Agent（最新版）</li><li>Provider: DeepSeek（官方 API）</li><li>认证方式：<code>DEEPSEEK_API_KEY</code> 环境变量</li></ul><h2 id="排查过程"><a href="#排查过程" class="headerlink" title="排查过程"></a>排查过程</h2><h3 id="1-配置结构分析"><a href="#1-配置结构分析" class="headerlink" title="1. 配置结构分析"></a>1. 配置结构分析</h3><p>Hermes 的凭证体系有两层：</p><p><strong><code>~/.hermes/.env</code></strong> — 环境变量文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DEEPSEEK_API_KEY=***</span><br></pre></td></tr></table></figure><p><strong><code>~/.hermes/config.yaml</code></strong> — 主配置，包含：</p><ul><li><code>providers.deepseek-official</code> — 官方 provider，通过 <code>key_env: DEEPSEEK_API_KEY</code> 引用上述变量</li><li><code>custom_providers</code> — 自定义 provider 列表，每个独立配置了 <code>api_key</code></li></ul><p>看起来没什么问题：环境变量是最新的 key，官方 provider 正确引用它。</p><h3 id="2-发现冲突"><a href="#2-发现冲突" class="headerlink" title="2. 发现冲突"></a>2. 发现冲突</h3><p>深入检查 <code>custom_providers</code>，发现一个叫 <code>muapi</code> 的 provider：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">api_key:</span> <span class="string">sk-old...-key</span></span><br><span class="line">  <span class="attr">base_url:</span> <span class="string">https://api.deepseek.com</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">muapi</span></span><br></pre></td></tr></table></figure><p>关键线索：<code>muapi</code> 和 <code>deepseek-official</code> 使用了<strong>完全相同的 base URL</strong>。</p><h3 id="3-凭证池机制"><a href="#3-凭证池机制" class="headerlink" title="3. 凭证池机制"></a>3. 凭证池机制</h3><p>Hermes 内部维护一个<strong>凭证池（credential pool）</strong>，以 base URL 为 key 缓存 provider 到 API key 的映射。当请求发往 <code>https://api.deepseek.com</code> 时：</p><ol><li>凭证池查找 base URL → 匹配到 <code>muapi</code>（而非 <code>deepseek-official</code>）</li><li>使用 <code>muapi</code> 缓存的旧密钥</li><li>旧密钥已过期 → <strong>401</strong></li></ol><p>日志中已有明确信号：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">run_agent: Credential pool provider mismatch:</span><br><span class="line">  pool=custom:muapi, agent=custom — skipping pool mutation</span><br></pre></td></tr></table></figure><p>凭证池已经被 <code>muapi</code> 的旧密钥锁定，即使切换 provider 也无法刷新。</p><h2 id="修复步骤"><a href="#修复步骤" class="headerlink" title="修复步骤"></a>修复步骤</h2><h3 id="Step-1-更新环境变量"><a href="#Step-1-更新环境变量" class="headerlink" title="Step 1: 更新环境变量"></a>Step 1: 更新环境变量</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ~/.hermes/.env</span></span><br><span class="line">DEEPSEEK_API_KEY=***</span><br></pre></td></tr></table></figure><h3 id="Step-2-同步更新冲突-provider"><a href="#Step-2-同步更新冲突-provider" class="headerlink" title="Step 2: 同步更新冲突 provider"></a>Step 2: 同步更新冲突 provider</h3><p>在 <code>config.yaml</code> 中找到 <code>muapi</code> 条目，将其 <code>api_key</code> 更新为与 <code>DEEPSEEK_API_KEY</code> 相同的新密钥。</p><p>**为什么？**凭证池缓存的是 base URL → key 的映射，不更新的话即使 <code>.env</code> 正确，请求仍然走旧密钥。</p><h3 id="Step-3-清除凭证池缓存"><a href="#Step-3-清除凭证池缓存" class="headerlink" title="Step 3: 清除凭证池缓存"></a>Step 3: 清除凭证池缓存</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -f ~/.hermes/state.db ~/.hermes/state.db-shm ~/.hermes/state.db-wal</span><br></pre></td></tr></table></figure><p>Hermes 将凭证映射缓存在 SQLite state.db 中，删除后强制重建。</p><h3 id="Step-4-重启-Gateway"><a href="#Step-4-重启-Gateway" class="headerlink" title="Step 4: 重启 Gateway"></a>Step 4: 重启 Gateway</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hermes gateway restart</span><br></pre></td></tr></table></figure><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 直测 API</span></span><br><span class="line">curl -X POST https://api.deepseek.com/v1/chat/completions \</span><br><span class="line">  -H <span class="string">&quot;Authorization: Bearer *** \</span></span><br><span class="line"><span class="string">  -H &quot;</span>Content-Type: application/json<span class="string">&quot; \</span></span><br><span class="line"><span class="string">  -d &#x27;&#123;&quot;</span>model<span class="string">&quot;:&quot;</span>deepseek-chat<span class="string">&quot;,&quot;</span>messages<span class="string">&quot;:[&#123;&quot;</span>role<span class="string">&quot;:&quot;</span>user<span class="string">&quot;,&quot;</span>content<span class="string">&quot;:&quot;</span>hi<span class="string">&quot;&#125;],&quot;</span>max_tokens<span class="string">&quot;:5&#125;&#x27;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"># 确认网关无 401</span></span><br><span class="line"><span class="string">tail -f ~/.hermes/logs/agent.log | grep -v &quot;</span>Authentication Fails<span class="string">&quot;</span></span><br></pre></td></tr></table></figure><h2 id="经验教训"><a href="#经验教训" class="headerlink" title="经验教训"></a>经验教训</h2><ol><li><strong>避免同一 base URL 对应多个 provider</strong> — 凭证池以 URL 为 key，同名端点会导致路由混乱</li><li><strong>轮换密钥后务必清除 <code>state.db</code></strong> — 旧的凭证缓存是静默杀手</li><li><strong>留意 <code>Credential pool provider mismatch</code> 警告</strong> — 这是凭证路由异常的直接信号，不要忽视</li></ol><p>当多个 provider 指向同一 API endpoint 时，凭证池的行为不是「按 provider 隔离」而是「按 URL 共享」—— 谁先注册谁占位。这个行为细节在文档中不容易注意到，但踩过一次以后就很清晰了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>逆向 115 网盘 API、安装 MCP 服务器、修复 Skill — 折腾记录</title>
      <link>https://b.xpai.uk/2026/06/11/115-skill-journey/</link>
      <description>
        <![CDATA[<h1 id="逆向-115-网盘-API、安装-MCP-服务器、修复-Skill-—-折腾记录"><a href="#逆向-115-网盘-API、安装-MCP-服务器、修复-Skill-—-折腾记录" class="headerlink" title="逆向 115 网盘 AP]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/API/">API</category>
      <category domain="https://b.xpai.uk/tags/115/">115</category>
      <category domain="https://b.xpai.uk/tags/MCP/">MCP</category>
      <category domain="https://b.xpai.uk/tags/Hermes/">Hermes</category>
      <category domain="https://b.xpai.uk/tags/%E9%80%86%E5%90%91/">逆向</category>
      <pubDate>Wed, 10 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="逆向-115-网盘-API、安装-MCP-服务器、修复-Skill-—-折腾记录"><a href="#逆向-115-网盘-API、安装-MCP-服务器、修复-Skill-—-折腾记录" class="headerlink" title="逆向 115 网盘 API、安装 MCP 服务器、修复 Skill — 折腾记录"></a>逆向 115 网盘 API、安装 MCP 服务器、修复 Skill — 折腾记录</h1><h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>拿到一个 115-publish 的 Hermes skill 包，功能很全：扫码登录、文件浏览、搜索、转存、离线下载。但装上一跑，全部 404。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QR_FAIL:Request failed with status code 404</span><br></pre></td></tr></table></figure><p>115 的 API 已经全面升级换代，旧的 skill 代码全废了。</p><h2 id="第一阶段：逆向-QR-登录-API"><a href="#第一阶段：逆向-QR-登录-API" class="headerlink" title="第一阶段：逆向 QR 登录 API"></a>第一阶段：逆向 QR 登录 API</h2><p>旧代码用的端点：</p><ul><li><code>webapi.115.com/qrcode/login</code> → 404</li><li><code>passport.115.com/qrcode/generate</code> → 404</li><li><code>passportapi.115.com/qrcode/generate</code> → 404</li></ul><p>全死了。得自己找新的。</p><h3 id="浏览器侦查"><a href="#浏览器侦查" class="headerlink" title="浏览器侦查"></a>浏览器侦查</h3><p>打开 115.com 的登录页面，找到”二维码登录”按钮。用 Chrome DevTools 摸到关键的 JavaScript 文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cdnassets.115.com/login/loginworker/loginWkApi.js</span><br></pre></td></tr></table></figure><p>反混淆后提取到关键信息——新的 QR 码域名：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">qrcodeapi.115.com</span><br></pre></td></tr></table></figure><h3 id="新-API-探针"><a href="#新-API-探针" class="headerlink" title="新 API 探针"></a>新 API 探针</h3><p>直接请求 token 端点：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">GET https://qrcodeapi.115.com/api/1.0/web/1.0/token</span><br><span class="line"></span><br><span class="line">→ &#123;</span><br><span class="line">  &quot;state&quot;:1,</span><br><span class="line">  &quot;data&quot;:&#123;</span><br><span class="line">    &quot;uid&quot;:&quot;5df54c49...&quot;,</span><br><span class="line">    &quot;time&quot;:1781179327,</span><br><span class="line">    &quot;sign&quot;:&quot;c4268934...&quot;,</span><br><span class="line">    &quot;qrcode&quot;:&quot;https://115.com/scan/dg-...&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>轮询状态：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">GET https://qrcodeapi.115.com/get/status/?uid=xxx&amp;time=xxx&amp;sign=xxx</span><br><span class="line"></span><br><span class="line">status:</span><br><span class="line">  0 = 等待扫码</span><br><span class="line">  1 = 已扫码待确认</span><br><span class="line">  2 = 登录成功</span><br><span class="line"> -1 = 过期</span><br><span class="line"> -2 = 取消</span><br></pre></td></tr></table></figure><p>登录确认（status&#x3D;2 后调用）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">POST https://passportapi.115.com/app/1.0/web/1.0/login/qrcode</span><br><span class="line">account=xxx&amp;app=web</span><br><span class="line"></span><br><span class="line">→ 返回 cookie: UID, CID, SEID, KID + Set-Cookie: USERSESSIONID</span><br></pre></td></tr></table></figure><h3 id="踩坑"><a href="#踩坑" class="headerlink" title="踩坑"></a>踩坑</h3><p>Cookie 字段名变了。旧格式用 <code>SE</code>，新格式用 <code>SEID</code>。而且多了一个 <code>USERSESSIONID</code>，缺了这个 sessionId 所有 API 都返回”登录超时”。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// 错误</span><br><span class="line">SE=xxx  → API 返回 &quot;登录超时&quot;</span><br><span class="line"></span><br><span class="line">// 正确</span><br><span class="line">SEID=xxx; USERSESSIONID=yyy → API 返回用户信息</span><br></pre></td></tr></table></figure><h2 id="第二阶段：离线下载遇阻"><a href="#第二阶段：离线下载遇阻" class="headerlink" title="第二阶段：离线下载遇阻"></a>第二阶段：离线下载遇阻</h2><p>登录搞定后，想试离线下载（lixian），结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">POST https://lixian.115.com/lixian</span><br><span class="line">→ &#123;&quot;state&quot;:false,&quot;error_msg&quot;:&quot;请升级新版本的115客户端&quot;&#125;</span><br></pre></td></tr></table></figure><p>115 已经把离线下载的 web API 关了，只能用他们的客户端。</p><h2 id="第三阶段：GitHub-寻宝"><a href="#第三阶段：GitHub-寻宝" class="headerlink" title="第三阶段：GitHub 寻宝"></a>第三阶段：GitHub 寻宝</h2><p>搜了一堆 115 离线下载的开源项目：</p><table><thead><tr><th>项目</th><th>状态</th><th>方法</th></tr></thead><tbody><tr><td><a href="https://github.com/coolzilj/lixian-115">lixian-115</a></td><td>❌ 2016 年停更</td><td>旧 web API 抓取</td></tr><tr><td><a href="https://github.com/coolzilj/lx115">lx115</a></td><td>❌ 2020 年停更</td><td>elevengo 库</td></tr><tr><td><a href="https://github.com/owtotwo/Offline-115">Offline-115</a></td><td>❌ 2021 年归档</td><td>cookie 抓取</td></tr><tr><td><a href="https://github.com/SeanClancy84/115-Download-request-forwarding">115-Download-request-forwarding</a></td><td>🟡 工具已过时</td><td>cookie 代理</td></tr><tr><td><a href="https://github.com/qiqiandfei/Telegram-115bot">Telegram-115bot</a></td><td>🟢 活跃维护 v3.4.2</td><td>115 Open Platform API</td></tr><tr><td><a href="https://github.com/lingwunb666/115-MCP-Server"><strong>115-MCP-Server</strong></a></td><td>🏆 <strong>活跃维护</strong></td><td><strong>115 Open API (官方)</strong></td></tr></tbody></table><p>重点看 <strong>115-MCP-Server</strong>：</p><ul><li>✅ 底层 <code>p115client</code> 库（最后更新：2 天前）</li><li>✅ 使用 115 <strong>官方 Open API</strong>（<code>proapi.115.com/open/offline/</code>），不是抓取</li><li>✅ 支持 ed2k&#x2F;磁力&#x2F;HTTP&#x2F;FTP&#x2F;种子离线下载</li><li>✅ 还是 MCP 服务器 → 可以直接挂到 Hermes 里</li></ul><h2 id="第四阶段：安装-115-MCP-Server"><a href="#第四阶段：安装-115-MCP-Server" class="headerlink" title="第四阶段：安装 115-MCP-Server"></a>第四阶段：安装 115-MCP-Server</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/lingwunb666/115-MCP-Server.git</span><br><span class="line"><span class="built_in">cd</span> 115-MCP-Server</span><br><span class="line">uv venv --python 3.12 .venv</span><br><span class="line">uv pip install -e .</span><br></pre></td></tr></table></figure><p>创建 Cookie 文件（从第一步登录拿到的数据）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UID=xxx; CID=xxx; SEID=xxx; KID=xxx; USERSESSIONID=xxx</span><br></pre></td></tr></table></figure><p>加到 Hermes 的 <code>config.yaml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">mcp_servers:</span></span><br><span class="line">  <span class="attr">115:</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">&quot;/path/to/.venv/bin/115-MCP-Server&quot;</span></span><br><span class="line">    <span class="attr">args:</span> [<span class="string">&quot;--transport&quot;</span>, <span class="string">&quot;stdio&quot;</span>]</span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">      <span class="attr">P115_COOKIES_PATH:</span> <span class="string">&quot;/path/to/115-cookies.txt&quot;</span></span><br></pre></td></tr></table></figure><p>验证服务正常：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> mcp_115_server.service <span class="keyword">import</span> P115Service</span><br><span class="line">svc = P115Service()</span><br><span class="line">info = svc.get_account_info()</span><br><span class="line"><span class="built_in">print</span>(info.keys())  <span class="comment"># → [&#x27;state&#x27;, &#x27;code&#x27;, &#x27;message&#x27;, &#x27;data&#x27;]</span></span><br></pre></td></tr></table></figure><h2 id="第五阶段：修复旧-Skill"><a href="#第五阶段：修复旧-Skill" class="headerlink" title="第五阶段：修复旧 Skill"></a>第五阶段：修复旧 Skill</h2><p>同时把旧的 <code>115-skills</code> 也修了。改动：</p><ol><li><strong>auth.js</strong> — QR 码 API 端点从 <code>webapi.115.com/qrcode/login</code> 换成 <code>qrcodeapi.115.com/api/1.0/web/1.0/token</code></li><li><strong>auth.js</strong> — Cookie 字段 <code>SE</code> → <code>SEID</code>，新增 <code>USERSESSIONID</code>&#x2F;<code>KID</code></li><li><strong>auth.js</strong> — 登录确认流程（status&#x3D;2 后调用 login&#x2F;qrcode 接口获取真实 cookie）</li><li><strong>http-client.js</strong> — Cookie 头格式修正，包含 <code>KID</code> 和 <code>USERSESSIONID</code></li><li><strong>cookie-store.js</strong> — 新增 <code>kid</code> 和 <code>sessionId</code> 字段存储</li></ol><h2 id="最终效果"><a href="#最终效果" class="headerlink" title="最终效果"></a>最终效果</h2><table><thead><tr><th>功能</th><th>旧 skill</th><th>修复后</th><th>MCP Server</th></tr></thead><tbody><tr><td>扫码登录</td><td>❌ 404</td><td>✅</td><td>✅</td></tr><tr><td>文件浏览</td><td>❌ 404</td><td>✅</td><td>✅</td></tr><tr><td>搜索文件</td><td>❌ 404</td><td>✅</td><td>✅</td></tr><tr><td>离线下载</td><td>❌ API 关闭</td><td>❌</td><td>✅</td></tr><tr><td>上传文件</td><td>❌</td><td>❌</td><td>✅</td></tr><tr><td>分享转存</td><td>❌ 404</td><td>❌</td><td>✅</td></tr></tbody></table><p>旧 skill 适合轻量浏览，MCP Server 提供完整能力（特别是离线下载）。</p><h2 id="链接汇总"><a href="#链接汇总" class="headerlink" title="链接汇总"></a>链接汇总</h2><ul><li><a href="https://github.com/lingwunb666/115-MCP-Server">115-MCP-Server</a> — 最终使用的 MCP 服务器</li><li><a href="https://github.com/chenyanggao/p115client">p115client</a> — 底层 115 Python 客户端库</li><li><a href="https://p115client.readthedocs.io/en/latest/">115 Open API 文档</a> — 官方 Open API 文档</li><li><a href="https://github.com/qiqiandfei/Telegram-115bot">Telegram-115bot</a> — 备选方案，Telegram 机器人</li><li><a href="https://github.com/deadblue/elevengo">elevengo</a> — Go 版 115 库</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>PDF 手绘批注技术实现</title>
      <link>https://b.xpai.uk/2026/06/10/pdf-ink-annotation-implementation/</link>
      <description>
        <![CDATA[<h1 id="PDF-手绘批注技术实现"><a href="#PDF-手绘批注技术实现" class="headerlink" title="PDF 手绘批注技术实现"></a>PDF 手绘批注技术实现</h1><h2 id="概述"><a href="#概述" class="]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/iOS/">iOS</category>
      <category domain="https://b.xpai.uk/tags/PDFKit/">PDFKit</category>
      <category domain="https://b.xpai.uk/tags/SwiftUI/">SwiftUI</category>
      <category domain="https://b.xpai.uk/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E7%8E%B0/">技术实现</category>
      <pubDate>Tue, 09 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="PDF-手绘批注技术实现"><a href="#PDF-手绘批注技术实现" class="headerlink" title="PDF 手绘批注技术实现"></a>PDF 手绘批注技术实现</h1><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>PDFHelper 的批注功能基于苹果原生 PDFKit 框架实现，未引入任何第三方 PDF 批注库。整个批注子系统约 300 行核心代码，分为三层：<strong>视图编排层</strong>、<strong>PDF 桥接层</strong>、<strong>手势处理层</strong>。</p><h2 id="架构设计"><a href="#架构设计" class="headerlink" title="架构设计"></a>架构设计</h2><h3 id="三层结构"><a href="#三层结构" class="headerlink" title="三层结构"></a>三层结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">PDFAnnotationView (SwiftUI View)</span><br><span class="line">    ↓ 数据驱动</span><br><span class="line">AnnotatablePDFView (UIViewRepresentable)</span><br><span class="line">    ↓ UIKit 桥接</span><br><span class="line">Coordinator (PDFViewDelegate + 手势处理)</span><br></pre></td></tr></table></figure><p><strong>PDFAnnotationView</strong> — SwiftUI 入口，接收 <code>ContentViewModel</code> 中的数据（文档、页码、工具选择、颜色），组合 PDF 视图和工具栏。</p><p><strong>AnnotatablePDFView</strong> — <code>UIViewRepresentable</code> 桥接，将 <code>PDFView</code>（UIKit 控件）嵌入 SwiftUI 树中。负责创建 PDFView、注册手势、同步数据。</p><p><strong>Coordinator</strong> — 核心，实现 <code>PDFViewDelegate</code> 协议，处理所有触摸事件和 PDFAnnotation 的创建与添加。</p><h3 id="为什么不用第三方库？"><a href="#为什么不用第三方库？" class="headerlink" title="为什么不用第三方库？"></a>为什么不用第三方库？</h3><ul><li>PDFKit 是 iOS 原生框架，零依赖、免维护</li><li><code>PDFAnnotation</code> + <code>PDFPage.addAnnotation</code> 覆盖了所有常用批注类型</li><li>避免 AliPlayerSDK 那样的二进制兼容问题</li><li>用户数据始终在本地，无需联网</li></ul><h2 id="手势系统"><a href="#手势系统" class="headerlink" title="手势系统"></a>手势系统</h2><h3 id="双手势分离"><a href="#双手势分离" class="headerlink" title="双手势分离"></a>双手势分离</h3><p>批注交互拆为两种手势：</p><table><thead><tr><th>手势</th><th>作用范围</th><th>目标工具</th></tr></thead><tbody><tr><td><code>UITapGestureRecognizer</code></td><td>所有非手绘工具</td><td>高亮、下划线、删除线、文本框、方形、圆形、备注</td></tr><tr><td><code>UIPanGestureRecognizer</code></td><td>仅手绘模式</td><td>手绘笔(ink)</td></tr></tbody></table><p>两者在 Coordinator 中通过 <code>handleTap(_:)</code> 和 <code>handlePan(_:)</code> 分别处理。手绘模式下 tap 被跳过（<code>tool != .ink</code> 提前 return），避免冲突。</p><h3 id="手势冲突处理"><a href="#手势冲突处理" class="headerlink" title="手势冲突处理"></a>手势冲突处理</h3><p><code>UIPanGestureRecognizer</code> 只允许单指操作：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">panGesture.maximumNumberOfTouches <span class="operator">=</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p>PDFView 默认有自己的滚动&#x2F;缩放手势，批注手势通过 Coordinator 的优先处理逻辑避免与滚动冲突——非手绘模式下 Pan 不注册，手绘模式下 Pan 优先于滚动。</p><h2 id="批注类型实现"><a href="#批注类型实现" class="headerlink" title="批注类型实现"></a>批注类型实现</h2><h3 id="1-高亮-下划线-删除线"><a href="#1-高亮-下划线-删除线" class="headerlink" title="1. 高亮 &#x2F; 下划线 &#x2F; 删除线"></a>1. 高亮 &#x2F; 下划线 &#x2F; 删除线</h3><p>这三种是 PDFKit 原生支持的标注类型，实现最为简单：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> annotation <span class="operator">=</span> <span class="type">PDFAnnotation</span>(bounds: bounds, forType: .highlight, withProperties: <span class="literal">nil</span>)</span><br><span class="line">annotation<span class="operator">?</span>.color <span class="operator">=</span> selectedColor.uiColor.withAlphaComponent(<span class="number">0.5</span>)  <span class="comment">// 高亮半透明</span></span><br><span class="line">page.addAnnotation(annotation)</span><br></pre></td></tr></table></figure><ul><li><code>.highlight</code> 类型自动渲染为典型的高亮背景色</li><li><code>.underline</code> 和 <code>.strikeOut</code> 直接使用选中颜色</li><li>用户点击即创建，附着在触摸点位置（100x20 固定区域）</li><li>后续可拖拽调整位置或缩放</li></ul><h3 id="2-手绘笔-Ink-—-核心实现"><a href="#2-手绘笔-Ink-—-核心实现" class="headerlink" title="2. 手绘笔 (Ink) — 核心实现"></a>2. 手绘笔 (Ink) — 核心实现</h3><p>手绘是批注中最复杂的部分，需要追踪连续触摸轨迹。</p><h4 id="UIBezierPath-轨迹追踪"><a href="#UIBezierPath-轨迹追踪" class="headerlink" title="UIBezierPath 轨迹追踪"></a>UIBezierPath 轨迹追踪</h4><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .began — 路径起点</span></span><br><span class="line">currentPath <span class="operator">=</span> <span class="type">UIBezierPath</span>()</span><br><span class="line">currentPath<span class="operator">?</span>.move(to: pagePoint)</span><br><span class="line"></span><br><span class="line"><span class="comment">// .changed — 逐点连线</span></span><br><span class="line">currentPath<span class="operator">?</span>.addLine(to: pagePoint)</span><br><span class="line"></span><br><span class="line"><span class="comment">// .ended — 生成 Ink 批注</span></span><br><span class="line"><span class="keyword">let</span> annotation <span class="operator">=</span> <span class="type">PDFAnnotation</span>(bounds: bounds, forType: .ink, withProperties: <span class="literal">nil</span>)</span><br><span class="line">annotation.color <span class="operator">=</span> selectedColor.uiColor</span><br><span class="line">annotation.border <span class="operator">=</span> <span class="type">PDFBorder</span>()</span><br><span class="line">annotation.border<span class="operator">?</span>.lineWidth <span class="operator">=</span> <span class="number">2</span></span><br><span class="line">annotation.add(path)    <span class="comment">// 路径注入</span></span><br><span class="line">page.addAnnotation(annotation)</span><br></pre></td></tr></table></figure><p><strong>关键点</strong>：</p><ul><li>坐标转换：<code>gesture.location(in: pdfView)</code> → <code>pdfView.convert(location, to: page)</code> 将屏幕坐标转为 PDF 页面坐标</li><li><code>PDFAnnotation.add(path)</code> 将 UIBezierPath 嵌入 PDF 原生 Ink 标注</li><li>边框扩展 <code>bounds.insetBy(dx: -10, dy: -10)</code> 避免笔迹被裁剪</li><li>每次 <code>.ended</code> 后清空 <code>currentPath</code>，准备下一次绘制</li></ul><h4 id="触摸流畅性"><a href="#触摸流畅性" class="headerlink" title="触摸流畅性"></a>触摸流畅性</h4><ul><li>不降采样、不贝塞尔简化——保持原始轨迹精度</li><li>直接在 <code>UIPanGestureRecognizer</code> 的 <code>.changed</code> 逐点累加</li><li>无需额外缓冲区或离屏渲染，PDFKit 的 Ink 渲染层已经硬件加速</li></ul><h3 id="3-文本框-FreeText"><a href="#3-文本框-FreeText" class="headerlink" title="3. 文本框 (FreeText)"></a>3. 文本框 (FreeText)</h3><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">annotation <span class="operator">=</span> <span class="type">PDFAnnotation</span>(bounds: <span class="operator">...</span>, forType: .freeText, withProperties: <span class="literal">nil</span>)</span><br><span class="line">annotation<span class="operator">?</span>.color <span class="operator">=</span> .clear                          <span class="comment">// 背景透明</span></span><br><span class="line">annotation<span class="operator">?</span>.fontColor <span class="operator">=</span> selectedColor.uiColor       <span class="comment">// 文字颜色</span></span><br><span class="line">(annotation <span class="keyword">as?</span> <span class="type">PDFAnnotation</span>)<span class="operator">?</span>.font <span class="operator">=</span> <span class="type">UIFont</span>.systemFont(ofSize: <span class="number">14</span>)</span><br></pre></td></tr></table></figure><ul><li>背景色设为 <code>.clear</code> 实现透明底文字</li><li><code>fontColor</code> 独立于背景色，支持 6 色切换</li><li>用户点击后创建可编辑文本框，支持键盘输入</li><li>再次点击可选中进入编辑模式</li></ul><h3 id="4-形状批注-方形-圆形"><a href="#4-形状批注-方形-圆形" class="headerlink" title="4. 形状批注 (方形 &#x2F; 圆形)"></a>4. 形状批注 (方形 &#x2F; 圆形)</h3><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">annotation <span class="operator">=</span> <span class="type">PDFAnnotation</span>(bounds: bounds, forType: .square, withProperties: <span class="literal">nil</span>)</span><br><span class="line">annotation<span class="operator">?</span>.color <span class="operator">=</span> selectedColor.uiColor.withAlphaComponent(<span class="number">0.3</span>)</span><br><span class="line">annotation<span class="operator">?</span>.interiorColor <span class="operator">=</span> selectedColor.uiColor.withAlphaComponent(<span class="number">0.1</span>)</span><br></pre></td></tr></table></figure><ul><li><code>.square</code> &#x2F; <code>.circle</code> 类型直接由 PDFKit 渲染</li><li>边框色 <code>color</code> + 填充色 <code>interiorColor</code> 分开设置</li><li>固定 80x80 大小，用户后续可拖拽调整</li></ul><h3 id="5-备注-Note"><a href="#5-备注-Note" class="headerlink" title="5. 备注 (Note)"></a>5. 备注 (Note)</h3><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">annotation <span class="operator">=</span> <span class="type">PDFAnnotation</span>(bounds: bounds, forType: .text, withProperties: <span class="literal">nil</span>)</span><br><span class="line">annotation<span class="operator">?</span>.color <span class="operator">=</span> selectedColor.uiColor</span><br><span class="line">annotation<span class="operator">?</span>.contents <span class="operator">=</span> <span class="string">&quot;备注&quot;</span></span><br></pre></td></tr></table></figure><ul><li>使用 <code>.text</code> 类型图标标记，PDFKit 自动渲染为便签图标</li><li>点击后可查看&#x2F;编辑 <code>contents</code> 文本</li></ul><h2 id="颜色系统"><a href="#颜色系统" class="headerlink" title="颜色系统"></a>颜色系统</h2><p>6 色循环，采用枚举驱动：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">AnnotationColor</span>: <span class="title class_ inherited__">CaseIterable</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> red, yellow, green, blue, purple, orange</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> color: <span class="type">Color</span> &#123; <span class="operator">...</span> &#125;       <span class="comment">// SwiftUI Color</span></span><br><span class="line">    <span class="keyword">var</span> uiColor: <span class="type">UIColor</span> &#123; <span class="operator">...</span> &#125;   <span class="comment">// UIColor 桥接</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工具栏中 <code>ColorPickerView</code> 显示 6 个圆形色块，选中标记打勾 ✓。</p><h2 id="工具栏设计"><a href="#工具栏设计" class="headerlink" title="工具栏设计"></a>工具栏设计</h2><p><code>AnnotationToolbar</code> 使用水平滚动卡片布局：</p><ul><li>8 种工具：高亮、下划线、删除线、手绘笔、文本框、方形、圆形、备注</li><li>每个工具 &#x3D; SF Symbol 图标 + 中文名称</li><li>选中状态高亮蓝色背景</li><li>颜色按钮在末尾，展开&#x2F;收起色盘</li></ul><h2 id="数据流"><a href="#数据流" class="headerlink" title="数据流"></a>数据流</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">用户触摸 → Coordinator → 创建 PDFAnnotation → page.addAnnotation → 回调 onAnnotationAdded</span><br><span class="line">                                                                              ↓</span><br><span class="line">                                                                    viewModel.saveAnnotations()</span><br><span class="line">                                                                              ↓</span><br><span class="line">                                                                    PDFDocument.write(to:)</span><br></pre></td></tr></table></figure><ul><li>每次添加批注后触发的 <code>onAnnotationAdded</code> 回调驱动持久化</li><li><code>viewModel.saveAnnotations()</code> 将当前 PDF 文档写回文件系统</li><li>批注直接嵌入 PDF 文件——用其他 PDF 阅读器打开同样可见</li></ul><h2 id="长按删除"><a href="#长按删除" class="headerlink" title="长按删除"></a>长按删除</h2><p>（实现位于 <code>ContentViewModel</code> 中，通过 <code>UILongPressGestureRecognizer</code> 识别）用户长按已存在的批注时：</p><ol><li>检测触摸点下的 <code>PDFAnnotation</code></li><li>弹出确认提示或直接删除</li><li><code>page.removeAnnotation(annotation)</code></li><li>重新保存文档</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整个批注子系统完全依赖苹果原生 PDFKit API，无第三方依赖。核心实现约 300 行 Swift 代码，涵盖 8 种批注类型。手绘部分使用 <code>UIBezierPath</code> + <code>UIPanGestureRecognizer</code> 逐点追踪轨迹，通过 <code>PDFAnnotation.add(path)</code> 注入 PDF 原生 Ink 标注，兼顾流畅度与兼容性。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Caddy + Cloudflare + 阿里云 ICP 备案踩坑记录</title>
      <link>https://b.xpai.uk/2026/06/09/caddy-icp-lesson/</link>
      <description>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>手里有一台阿里云 ECS（Ubuntu 22.04），IP <code>SERVER_IP</code>，上面跑着一个服务监听 <code>]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/%E8%BF%90%E7%BB%B4/">运维</category>
      <category domain="https://b.xpai.uk/tags/Caddy/">Caddy</category>
      <category domain="https://b.xpai.uk/tags/Cloudflare/">Cloudflare</category>
      <pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>手里有一台阿里云 ECS（Ubuntu 22.04），IP <code>SERVER_IP</code>，上面跑着一个服务监听 <code>12201</code> 端口。</p><p>想通过域名 <code>f.example.com</code> 用 HTTPS 访问这个服务，于是开始了折腾之旅。</p><h2 id="第一步：安装-Caddy"><a href="#第一步：安装-Caddy" class="headerlink" title="第一步：安装 Caddy"></a>第一步：安装 Caddy</h2><p>Caddy 是现代 Web 服务器，自动 HTTPS 是卖点。SSH 到服务器后：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">apt update</span><br><span class="line">apt install -y debian-keyring debian-archive-keyring apt-transport-https curl</span><br><span class="line">curl -1sLf <span class="string">&#x27;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&#x27;</span> | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg</span><br><span class="line">curl -1sLf <span class="string">&#x27;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&#x27;</span> | <span class="built_in">tee</span> /etc/apt/sources.list.d/caddy-stable.list</span><br><span class="line">apt update &amp;&amp; apt install caddy -y</span><br></pre></td></tr></table></figure><p>配置 <code>/etc/caddy/Caddyfile</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">f.example.com &#123;</span><br><span class="line">    reverse_proxy localhost:12201</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重启 Caddy：<code>systemctl restart caddy</code></p><h2 id="第二步：DNS-配置"><a href="#第二步：DNS-配置" class="headerlink" title="第二步：DNS 配置"></a>第二步：DNS 配置</h2><p>域名 <code>example.com</code> 托管在 Cloudflare，添加 A 记录：</p><ul><li>类型: A</li><li>名称: f</li><li>内容: <code>SERVER_IP</code></li><li>代理: 橙色云朵（开启 Cloudflare 代理）</li></ul><h2 id="第三步：SSL-525-错误"><a href="#第三步：SSL-525-错误" class="headerlink" title="第三步：SSL 525 错误"></a>第三步：SSL 525 错误</h2><p>访问 <code>https://f.example.com</code>，返回 <strong>Cloudflare 525 SSL Handshake Failed</strong>。</p><p>原因分析：</p><ul><li>Caddy 尝试用 Let’s Encrypt 申请证书</li><li>但 Cloudflare 代理拦截了 HTTP-01 验证请求</li><li>证书申请失败，Caddy 没有有效证书</li><li>Cloudflare 用 HTTPS 连源站，握手失败</li></ul><h3 id="解决方案：Cloudflare-Origin-Certificate"><a href="#解决方案：Cloudflare-Origin-Certificate" class="headerlink" title="解决方案：Cloudflare Origin Certificate"></a>解决方案：Cloudflare Origin Certificate</h3><p>在 Cloudflare 面板：<code>SSL/TLS</code> → <code>源服务器</code> → <code>创建证书</code></p><p>获得证书和私钥后，上传到服务器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /etc/caddy/certs</span><br><span class="line"><span class="comment"># 上传 cert.pem 和 key.pem 到 /etc/caddy/certs/</span></span><br><span class="line"><span class="built_in">chown</span> -R caddy:caddy /etc/caddy/certs</span><br></pre></td></tr></table></figure><p>修改 Caddyfile：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">f.example.com &#123;</span><br><span class="line">    tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem</span><br><span class="line">    reverse_proxy localhost:12201</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重启后 SSL 525 错误解决。</p><h2 id="第四步：ICP-备案拦截"><a href="#第四步：ICP-备案拦截" class="headerlink" title="第四步：ICP 备案拦截"></a>第四步：ICP 备案拦截</h2><p>以为大功告成，结果访问返回 <strong>403 Forbidden</strong>，页面内容：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">title</span>&gt;</span>Non-compliance ICP Filing<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br></pre></td></tr></table></figure><p>阿里云会对未备案域名拦截 80&#x2F;443 端口的 HTTP&#x2F;HTTPS 请求。这是中国法规要求，云服务商强制执行。</p><h3 id="尝试绕过"><a href="#尝试绕过" class="headerlink" title="尝试绕过"></a>尝试绕过</h3><p>关闭 Cloudflare 代理（灰色云朵），让流量直连服务器 → 依然被拦截。</p><p>开启 Cloudflare 代理（橙色云朵）→ 流量经过 Cloudflare 到源站，源站依然返回 ICP 拦截页。</p><p>结论：<strong>只要源站在国内，80&#x2F;443 端口的请求都会被阿里云拦截</strong>，无论是否经过 Cloudflare。</p><h2 id="最终方案"><a href="#最终方案" class="headerlink" title="最终方案"></a>最终方案</h2><p>使用非标端口（如 8443）绕过 ICP 检查：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">:8443 &#123;</span><br><span class="line">    tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem</span><br><span class="line">    reverse_proxy localhost:12201</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>访问地址变为 <code>https://f.example.com:8443</code>。</p><p>注意：阿里云安全组需要放行 8443 端口（TCP 入方向）。</p><h2 id="踩坑总结"><a href="#踩坑总结" class="headerlink" title="踩坑总结"></a>踩坑总结</h2><ol><li><strong>Caddy 自动 HTTPS + Cloudflare 代理冲突</strong>：Cloudflare 拦截了 Let’s Encrypt 的验证请求，需要手动配置 Cloudflare Origin Certificate</li><li><strong>阿里云 ICP 备案拦截</strong>：国内服务器 80&#x2F;443 端口对未备案域名强制拦截，无法通过 Cloudflare 绕过</li><li><strong>Cloudflare Origin Certificate 私钥格式</strong>：粘贴时注意行尾空格，可能导致 SSL 握手失败</li><li><strong>fail2ban 封禁</strong>：频繁 SSH 连接可能触发 fail2ban，导致无法远程管理服务器</li></ol><h2 id="经验教训"><a href="#经验教训" class="headerlink" title="经验教训"></a>经验教训</h2><ul><li>国内服务器 + 未备案域名 &#x3D; 只能用非标端口</li><li>Cloudflare 代理 + 自定义证书 &#x3D; 需要 Origin Certificate</li><li>域名解析用 Cloudflare DNS，但代理功能根据需求开关</li><li>重要操作前先检查 fail2ban 状态，避免被封 IP</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hello World</title>
      <link>https://b.xpai.uk/2026/06/09/hello-world/</link>
      <description>
        <![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for]]>
      </description>
      <author>mcx</author>
      <pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p><h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p><h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p><h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Claude API 的三种来源及其区别</title>
      <link>https://b.xpai.uk/2026/06/09/claude-api-types/</link>
      <description>
        <![CDATA[<p>很多人不知道，Claude 的官方 API 其实分为三种来源，价格、可用范围和质量各不相同。</p>
<h2 id="一、官方-API"><a href="#一、官方-API" class="headerlink" title="一、官方 API"></a>一、官方 API]]>
      </description>
      <author>mcx</author>
      <category domain="https://b.xpai.uk/tags/API/">API</category>
      <category domain="https://b.xpai.uk/tags/AI/">AI</category>
      <category domain="https://b.xpai.uk/tags/Claude/">Claude</category>
      <pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>很多人不知道，Claude 的官方 API 其实分为三种来源，价格、可用范围和质量各不相同。</p><h2 id="一、官方-API"><a href="#一、官方-API" class="headerlink" title="一、官方 API"></a>一、官方 API</h2><p><strong>来源</strong>：直接在 <a href="https://console.anthropic.com/">console.anthropic.com</a> 注册，绑定信用卡充值。</p><p><strong>价格</strong>：按 token 计费，明码标价。比如 Claude Sonnet 4：</p><ul><li>输入：$3 &#x2F; 百万 token</li><li>输出：$15 &#x2F; 百万 token</li></ul><p><strong>特点</strong>：</p><ul><li>稳定、正规、有 SLA</li><li>需要实名认证（部分地区）</li><li>有速率限制，按充值金额提升</li><li>支持全部模型和功能</li></ul><p><strong>适合</strong>：企业、正规项目、需要稳定性的场景。</p><hr><h2 id="二、Claude-Max-订阅反代"><a href="#二、Claude-Max-订阅反代" class="headerlink" title="二、Claude Max 订阅反代"></a>二、Claude Max 订阅反代</h2><p><strong>来源</strong>：订阅 Claude Max（$100&#x2F;月 或 $200&#x2F;月），通过第三方工具把网页版能力转成 API 接口。</p><p><strong>价格</strong>：固定月费，不限量（有每日&#x2F;每月上限）。</p><p><strong>特点</strong>：</p><ul><li>月费固定，用得多就划算</li><li>第三方反代工具，非官方支持</li><li>可能被封号（违反 ToS）</li><li>模型版本跟随网页版</li></ul><p><strong>适合</strong>：个人重度用户、想省钱的开发者。</p><hr><h2 id="三、逆向反代"><a href="#三、逆向反代" class="headerlink" title="三、逆向反代"></a>三、逆向反代</h2><p><strong>来源</strong>：通过逆向工程模拟网页请求，或使用泄露的 session&#x2F;token。</p><p><strong>价格</strong>：通常极低甚至免费。</p><p><strong>特点</strong>：</p><ul><li>不稳定，随时可能失效</li><li>质量参差不齐</li><li>有安全风险（数据经过第三方）</li><li>可能使用被盗账号</li></ul><p><strong>适合</strong>：临时测试、不重要的场景。<strong>不推荐用于生产。</strong></p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>类型</th><th>价格</th><th>稳定性</th><th>合规性</th><th>推荐度</th></tr></thead><tbody><tr><td>官方 API</td><td>按量付费</td><td>高</td><td>完全合规</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td>Max 反代</td><td>固定月费</td><td>中</td><td>灰色地带</td><td>⭐⭐⭐</td></tr><tr><td>逆向反代</td><td>极低</td><td>低</td><td>违规</td><td>⭐</td></tr></tbody></table><p><strong>建议</strong>：正规项目用官方 API，个人折腾可以试试 Max 反代，逆向反代不推荐。</p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
