<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sanya</title><description>Sanya&apos;s Blog</description><link>https://www.sanyablog.cn/</link><templateTheme>Sanya</templateTheme><templateThemeVersion>6.13.5</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年9月7日 02:32:32</lastBuildDate><item><title>博客 AI 助手“无回复”根因：反代网关地址缓存过期导致 502</title><link>https://www.sanyablog.cn/posts/nginx-stale-upstream-502-fix-20260907/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/nginx-stale-upstream-502-fix-20260907/</guid><description>博客 AI 助手无回复的排查经历：根因是 Nginx 反代对后端容器名解析出旧地址导致 502；通过容器内 DNS + 变量转发，让网关每次请求重新解析后端容器地址实现自愈。</description><pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;博客 AI 助手“无回复”的完整排查记录：根因不是服务崩溃、也不是会话丢失，而是 Nginx 反向代理在后端容器重建后仍沿用缓存中的旧地址向服务端转发，导致网关错误（502）。修复方式是借助容器网络内置 DNS + 变量转发，让网关每次请求都重新解析后端容器名，从而在约 10 秒内自动跟上容器地址变化，从根上消除这一整类问题。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;问题&lt;a href=&quot;#问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;博客页面的 AI 助手平时正常，某天突然“一直弹提示、问什么都不回复”，同一批老对话也续不上。客户端始终拿不到访问令牌，任何对话请求都无响应。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;根因&lt;a href=&quot;#根因&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;前端每次发 Coze 消息前，先向后端网关请求一次访问令牌（&lt;code&gt;POST /api/coze/token/init&lt;/code&gt;）。这一步被网关以 502 拒绝。真正根因不是后端崩溃、不是内存耗尽、也不是会话/刷新逻辑，而是 Nginx 反向代理的【上游地址缓存过期】。&lt;/p&gt;&lt;p&gt;Nginx 在启动或重载的那一刻，会把后端容器名一次性解析成内网地址并缓存下来。本次把后端从单体拆成独立容器（微服务拆分）后触发了容器重建，容器拿到新地址；但 Nginx 仍拿着缓存里的旧地址去连接。旧地址此刻已被另一个容器占用、且对应服务的监听端口也变了，于是连接被拒、向上游抛 502。后端应用本身完全健康，直连容器地址仍返回 200。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;修复方案&lt;a href=&quot;#修复方案&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;让 Nginx 改成“每次请求都重新解析”的方式：在配置顶层启用容器内网 DNS 解析器，并把各后端反代入口从“固定上游”改为“变量转发”。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 容器网络内置 DNS（127.0.0.11），供变量 proxy_pass 每次请求重新解析&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;resolver &lt;/span&gt;&lt;span&gt;127.0.0.11&lt;/span&gt;&lt;span&gt; valid=10s ipv6=off;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /api/coze/ {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 变量形式转发 → 每次请求都经内网 DNS 解析容器名&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;set &lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;coze_upstream&lt;/span&gt;&lt;span&gt; http://coze-gateway:8080;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_pass &lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;coze_upstream&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;关键点：必须用「变量 &lt;code&gt;proxy_pass&lt;/code&gt; + &lt;code&gt;resolver&lt;/code&gt;」；&lt;code&gt;server … resolve&lt;/code&gt;（upstream 自动解析）是 Nginx Plus 才有的能力，开源版 Nginx（&lt;code&gt;nginx:alpine&lt;/code&gt;）只能走这套。容器重建换 IP 后，网关约 10 秒内自动跟上，无需任何手工重载。对每个需要转发到后端容器的入口都做同样处理；一些独立、负载小、极少重建的入口（如私有包仓库）保持原有写法不动。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;如何应用&lt;a href=&quot;#如何应用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;修改网关配置模板（&lt;code&gt;server/nginx/default.conf.template&lt;/code&gt;）：顶层加 &lt;code&gt;resolver&lt;/code&gt;，各后端反代入口改成变量 &lt;code&gt;proxy_pass&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;渲染后用临时容器校验（&lt;code&gt;nginx -t&lt;/code&gt;）。注意：校验容器必须挂到容器网络，否则解析不到尚在用的旧内联上游，会误报“找不到主机”——那是校验环境限制，不是配置问题。&lt;/li&gt;
&lt;li&gt;先备份现有配置再替换，之后重启网关容器。因替换会生成新的文件节点，需【重启而非仅重载】才能让新配置生效。&lt;/li&gt;
&lt;li&gt;验证：&lt;code&gt;POST /api/coze/token/init&lt;/code&gt; 返回 200；首页 200；未带每日 JWT 的 &lt;code&gt;POST /api/dify/chat&lt;/code&gt; 返回 401（说明已路由到 Dify 网关而非错上游）。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;一条小坑&lt;a href=&quot;#一条小坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;发布到 Astro 时，若 frontmatter 的 &lt;code&gt;tags&lt;/code&gt; 序列里写到裸数字（如 &lt;code&gt;502&lt;/code&gt;），会被 YAML 强转成 &lt;code&gt;number&lt;/code&gt;，而 &lt;code&gt;posts&lt;/code&gt; 集合 schema 要求 &lt;code&gt;tags&lt;/code&gt; 全为 &lt;code&gt;string&lt;/code&gt;，构建会报 &lt;code&gt;InvalidContentEntryDataError&lt;/code&gt;。需要加引号写成 &lt;code&gt;&quot;502&quot;&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这条路径从 2026 年 9 月 7 日实测通过，彻底消除了容器重建导致的网关地址漂移 502。核心价值是把“地址缓存过期导致网关错误”这一整类问题从根上解决，同时不引入手工重启或重载的运维负担。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>让博客文章携带可下载文件：随文附件功能实现记录</title><link>https://www.sanyablog.cn/posts/blog-attachment-download/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/blog-attachment-download/</guid><description>给静态博客加上随文附件下载功能：附件不随整站部署，单独上传到服务器专用目录，正文用超链接即可下载；记录了重试兜底、只读挂载点踩坑与下载限流。</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写技术文章时，经常想分享脚本、样例配置或打包好的工具。以前只能把内容整段贴进正文，或者丢到外部网盘——贴正文太占篇幅，网盘要跳转还要担心失效。最理想的形态是像写论文一样：正文里嵌一个下载链接，读者点击即得文件。这篇文章记录这次「随文附件下载」改造的思路、设计与踩坑。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;需求背景&lt;a href=&quot;#需求背景&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;博客原本只有文字和图片。遇到要分发文件（示例代码包、配置文件、小型工具）时，没有合适的载体。外部网盘体验差，临时粘贴又不可复用。希望达到的效果是：&lt;strong&gt;每篇文章可以有若干附件，附件以超链接形式嵌入正文，点击触发浏览器下载&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;设计决策：附件不随整站部署&lt;a href=&quot;#设计决策附件不随整站部署&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;第一个直觉是：把附件放进站点工程目录，随整站一起发布。但这个博客是静态站，每次发布都会把全站构建产物整体打包上传到服务器。如果把附件放进构建产物，每次部署都要全量搬运这些大文件，越积越大，拖慢部署。&lt;/p&gt;&lt;p&gt;于是改为&lt;strong&gt;双通道&lt;/strong&gt;设计：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;文章正文、样式、页面等，走原有的整站发布流程；&lt;/li&gt;
&lt;li&gt;附件文件，走独立的上传通道，直接送到服务器上一个专用目录，不进入站点工程仓库；&lt;/li&gt;
&lt;li&gt;Web 服务器把该专用目录挂载到站点文件路径的某个子路径下，对外提供下载。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这样附件与文章内容解耦：改正文不用重传附件，传新附件也不会触发全站重建。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;实现要点&lt;a href=&quot;#实现要点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;上传脚本&lt;/strong&gt;。本地一个脚本，从本地配置文件读取服务器地址、登录用户、密钥等连接信息（本地配置不进公开仓库）。执行时把指定文件或目录通过加密通道传送到服务器的附件目录，按文章标识分子目录存放。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;失败重试与人工兜底&lt;/strong&gt;。网络环境不稳定时，上传可能中途失败。脚本内置重试：自动重试数次、每次间隔几秒。多次失败后不再盲目重试，而是&lt;strong&gt;非零退出并打印排查指引&lt;/strong&gt;——连通性检查、磁盘空间检查、手动上传命令。把”修不修”的判断交还给人，而不是静默失败或无限重试。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;服务器挂载&lt;/strong&gt;。附件目录以只读方式挂载进 Web 服务器的容器，映射到站点文件路径下的 &lt;code&gt;/files/&lt;/code&gt; 子路径。浏览器访问该路径时，Web 服务器按静态文件处理，直接返回文件内容并触发下载。文件名即下载后得到的文件名，无需额外路由逻辑。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;正文引用&lt;/strong&gt;。文章里用标准的 markdown 绝对链接指向附件的线上地址，例如「&lt;a href=&quot;/files/%E6%9F%90%E6%96%87%E7%AB%A0%E6%A0%87%E8%AF%86/%E6%9F%90%E6%96%87%E4%BB%B6&quot;&gt;下载说明&lt;/a&gt;」。格式简单，任何 markdown 编辑器都能写。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;踩坑：只读目录下创建不了挂载点&lt;a href=&quot;#踩坑只读目录下创建不了挂载点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;改造时遇到一个隐蔽问题：站点根目录是以&lt;strong&gt;只读&lt;/strong&gt;方式挂载进容器的，而附件目录要作为它的子目录&lt;strong&gt;再挂载一次&lt;/strong&gt;。容器在只读文件系统里尝试创建新挂载点时报错，直接导致容器启动失败。&lt;/p&gt;&lt;p&gt;排查发现，docker 在挂载嵌套路径时，如果内层挂载点目录还不存在，会尝试自动创建——但这一步发生在只读的根目录里，必然失败。解决方式很朴素：&lt;strong&gt;启动容器前，先在服务器上把附件挂载点目录预先建好&lt;/strong&gt;，再启动容器。目录已存在，嵌套挂载就成功了。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;下载限流：防滥用与防带宽占满&lt;a href=&quot;#下载限流防滥用与防带宽占满&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;附件走公开的静态下载路径，理论上任何人都能访问。为了让功能可以长期稳定对外，在 Web 服务器层给下载路径加了两道限流：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求频率限制&lt;/strong&gt;。按访问来源 IP 计数：每个 IP 平均每秒最多 5 次下载请求，允许短时间内突发到 20 次，超过的部分直接返回「请求过多」状态码，提示稍后再试。这一道挡住脚本批量刷下载、盗链拉取等滥用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载速度限制&lt;/strong&gt;。每个下载连接在文件前 8MB 全速传输，之后限制到每秒 2MB 以内。单个用户即使反复下载，也占不满服务器带宽，不影响其他访客正常浏览。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;另外给下载路径关闭了缓存（每次都拿最新文件）——附件更新后，读者重新下载立刻得到新版本，不会被旧缓存误导。两道限流的数值都集中在服务器配置里，方便按实际流量调整。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;使用方法&lt;a href=&quot;#使用方法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;发布带附件的文章，三步即可：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;本地暂存&lt;/strong&gt;：把附件放到本地暂存目录，按文章标识分子目录存放（该目录已排除出发布流程）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上传&lt;/strong&gt;：运行上传脚本，传入附件路径和文章标识，脚本自动传到服务器的附件目录并打印线上地址；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用&lt;/strong&gt;：在文章正文里用下载链接指向该线上地址。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;上传脚本会自动重试、失败给出排查指引；若附件含敏感信息（密钥、内网地址、服务器路径），按规则不发到线上，改用占位说明。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;配套：mdoc 定制化工具包&lt;a href=&quot;#配套mdoc-定制化工具包&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这套博客背后还有一套定制化的「文档管理 + 发布流水线」技能（归档修复方案 → 审查 → 生成博客文章 → 自动发布）。为了让它能迁移到另一台机器，我把定制化的技能、子代理和配置打包成了便携工具包，并写了完整的部署说明——详见 &lt;strong&gt;&lt;a href=&quot;/posts/mdoc-toolkit-deploy/&quot;&gt;mdoc 定制化工具包部署指南&lt;/a&gt;&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>mdoc 定制化工具包：把修复文档管理系统搬到新机器</title><link>https://www.sanyablog.cn/posts/mdoc-toolkit-deploy/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/mdoc-toolkit-deploy/</guid><description>一套便携部署工具包，在新机器上重建定制化的修复文档管理系统：技能、子代理、CLI 与配置模板全部就绪，按配置清单填好路径即可获得同等功能，附带 Claude Code 用法与 AI 部署教程。</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 AI 编程助手时，我长期维护着一套「修复方案文档管理系统」（mdoc）：每次排查、修复、改造的结论都会被归档成结构化文档，形成可搜索的个人知识库；在此基础上还有一套发布流水线，能把归档内容去敏后自动变成博客文章。这套系统是高度定制化的——命令协议、发布规则、归档格式都经过多轮打磨。本文分享的，是把它&lt;strong&gt;完整迁移到另一台机器&lt;/strong&gt;的便携工具包。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;mdoc 是什么&lt;a href=&quot;#mdoc-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;mdoc 是一个「把修复经验沉淀成文档」的个人系统：用 &lt;code&gt;/mdoc&lt;/code&gt; 斜杠命令管理文档，分类、命名、索引、搜索全部由 CLI 程序保证一致性；文档采用统一的格式规范，并附带一套「归档 → 审查 → 发布」的博客流水线。它的前世今生记录在 &lt;strong&gt;&lt;a href=&quot;/posts/mdoc-refactor-to-cli-library/&quot;&gt;《mdoc 重构：从 Claude Code skill 到任意目录可解析的 CLI 工具库》&lt;/a&gt;&lt;/strong&gt; 一文里。&lt;/p&gt;&lt;p&gt;问题在于：这套系统由技能、子代理、CLI 工具和配置文件共同组成，散落在本机的几个目录里，路径还是硬编码的。换一台机器就要全部重配，几乎不可能手动复刻。于是我把它们整理打包，做成了一个&lt;strong&gt;自包含、可移植、无敏感信息&lt;/strong&gt;的部署工具包。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;工具包里有什么&lt;a href=&quot;#工具包里有什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;解压后包含四类内容：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;内容&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;CLI 工具本体&lt;/td&gt;&lt;td&gt;纯 Python、零第三方依赖的安装包，可离线安装，负责文档系统的全部确定性操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;主技能&lt;/td&gt;&lt;td&gt;命令协议与发布规则：搜索、列表、创建、更新、删除、发布规范&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;发布流水线技能 + 三个子代理&lt;/td&gt;&lt;td&gt;归档 → 审查 → 发布一条龙；三个子代理各自独立工作，减少幻觉&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;配置模板&lt;/td&gt;&lt;td&gt;文档库路径、索引文件名、分类规则、内容风格等&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;工具包内&lt;strong&gt;不含任何敏感信息&lt;/strong&gt;：所有个人路径都被替换成了 &lt;code&gt;{{占位符}}&lt;/code&gt;，安装时按配置清单填好即可。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;下载与三步部署&lt;a href=&quot;#下载与三步部署&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;📥 &lt;a href=&quot;/files/mdoc-toolkit-deploy/mdoc-deploy-bundle.zip&quot;&gt;&lt;strong&gt;下载 mdoc 部署工具包&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;拿到压缩包后，三步即可完成部署：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装 CLI&lt;/strong&gt;：用 pip 安装包内的 wheel 文件，运行版本命令验证；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;放置技能与子代理&lt;/strong&gt;：把解压后的 skills 和 agents 目录复制到 Claude Code 的用户配置目录（Windows 在 &lt;code&gt;C:\Users\&amp;lt;你&amp;gt;\.claude\&lt;/code&gt;，macOS/Linux 在 &lt;code&gt;~/.claude/&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：复制配置模板为 &lt;code&gt;~/.mdoc.toml&lt;/code&gt;，按下表填写占位符。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;需要设置的部分（配置清单）&lt;a href=&quot;#需要设置的部分配置清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;占位符&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;th&gt;是否必填&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;文档库目录&lt;/td&gt;&lt;td&gt;存放修复方案文档和索引文件的目录（绝对路径）&lt;/td&gt;&lt;td&gt;必填&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;博客项目目录&lt;/td&gt;&lt;td&gt;静态博客项目根目录（发布流水线需要）&lt;/td&gt;&lt;td&gt;仅用发布功能时&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;服务器附件目录&lt;/td&gt;&lt;td&gt;博客随文附件的服务器存放目录&lt;/td&gt;&lt;td&gt;仅用附件上传时&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;填好文档库目录后，系统功能即与当前机器一致；博客项目和附件相关项只在你需要发布流水线时才配置。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;使用（以 Claude Code 为例）&lt;a href=&quot;#使用以-claude-code-为例&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;部署完成后，在 Claude Code 会话里直接用斜杠命令：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mdoc 关键词&lt;/code&gt; — 搜索修复方案文档&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mdoc -l&lt;/code&gt; — 列出文档&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mdoc -c 标题&lt;/code&gt; — 新建文档&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mdoc -u 参考名&lt;/code&gt; — 更新文档&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mdoc -d 参考名&lt;/code&gt; — 删除文档&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;完整命令清单在技能文件里有速查表，也可以跑 &lt;code&gt;/mdoc --help&lt;/code&gt; 查看。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;AI 部署教程&lt;a href=&quot;#ai-部署教程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;不想手动操作？把工具包交给新机器上的 AI 助手（Claude Code 或任何 AI CLI），粘贴下面的提示词即可：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;请帮我在当前机器上部署 mdoc 工具包。工具包已解压到 &lt;code&gt;某目录&lt;/code&gt;。&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;用 pip 安装其中的 CLI 安装包；&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;skills/&lt;/code&gt; 和 &lt;code&gt;agents/&lt;/code&gt; 复制到我的 Claude Code 用户配置目录；&lt;/li&gt;
&lt;li&gt;阅读配置模板，帮我创建 &lt;code&gt;~/.mdoc.toml&lt;/code&gt;，把文档库目录填成我指定的路径；&lt;/li&gt;
&lt;li&gt;检查技能与子代理文件里的占位符，替换成我的实际路径；&lt;/li&gt;
&lt;li&gt;运行版本命令和列表命令验证可用。&lt;/li&gt;
&lt;/ol&gt;&lt;/blockquote&gt;&lt;p&gt;AI 会按提示逐步完成安装、放置和配置，最后给出验证结果。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;安全说明&lt;a href=&quot;#安全说明&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;工具包&lt;strong&gt;不含&lt;/strong&gt;服务器地址、密钥、个人机器路径等敏感信息；&lt;/li&gt;
&lt;li&gt;所有需要按本机环境填写的内容都以占位符形式给出，并集中列在配置清单里；&lt;/li&gt;
&lt;li&gt;如果新机器与本文场景不同，只需在替换占位符时同步调整技能中的相关说明即可。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;关联阅读&lt;a href=&quot;#关联阅读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;工具包在博客上以附件形式提供下载，所用的随文附件机制见 &lt;strong&gt;&lt;a href=&quot;/posts/blog-attachment-download/&quot;&gt;《让博客文章携带可下载文件：随文附件功能实现记录》&lt;/a&gt;&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;mdoc 系统的设计历程见 &lt;strong&gt;&lt;a href=&quot;/posts/mdoc-refactor-to-cli-library/&quot;&gt;《mdoc 重构：从 Claude Code skill 到任意目录可解析的 CLI 工具库》&lt;/a&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;</content:encoded></item><item><title>mdoc 重构：从 Claude Code skill 到任意目录可解析的 CLI 工具库</title><link>https://www.sanyablog.cn/posts/mdoc-refactor-to-cli-library/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/mdoc-refactor-to-cli-library/</guid><description>把 /mdoc 个人 skill 重构为 core+CLI 分层、打包分发，再到 0.1.1 版本支持任意目录解析文档库的完整过程：架构决策、阶段迁移、验收踩坑与配置解析优先级定稿</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;作为 Claude Code 的个人技能，&lt;code&gt;/mdoc&lt;/code&gt; 曾经把修复方案文档管理的规则全部写死在 SKILL.md 里，文档存放在 auto-memory 目录。用得越久问题越明显：规则散落在各处、命令直接读写文档文件、路径硬编码、也无法分发给别人使用。&lt;/p&gt;
&lt;p&gt;这篇文章记录了我把 &lt;code&gt;/mdoc&lt;/code&gt; 重构为可安装 CLI 工具库的全过程：从 core+CLI 三层分层、打包分发，到 0.1.1 版本支持任意目录解析文档库。项目已开源在 &lt;a href=&quot;https://github.com/sanya2485/mdoc&quot; target=&quot;_blank&quot;&gt;github.com/sanya2485/mdoc&lt;/a&gt;。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;背景与目标&lt;a href=&quot;#背景与目标&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;原来的 &lt;code&gt;/mdoc&lt;/code&gt; skill 把文档管理规则写死在 SKILL.md 里，问题集中在四个方面：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;规则散落，维护困难&lt;/li&gt;
&lt;li&gt;命令直接 Read/Write 文档文件，缺少抽象&lt;/li&gt;
&lt;li&gt;路径硬编码在 skill 中，换个环境就失效&lt;/li&gt;
&lt;li&gt;无法分发，只能自己用&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;重构目标很明确：&lt;strong&gt;把确定性操作锁进代码&lt;/strong&gt;——分类、kebab-case 文件名、frontmatter、索引同步、搜索、校验等全部交给程序；skill 退化为 LLM 前端，只负责驱动 CLI。最终打包成可安装产物，分发给 Claude Code 用户各用各的库。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总体架构与决策&lt;a href=&quot;#总体架构与决策&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;采用三层架构，单一事实来源在 core：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;组件&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;core&lt;/td&gt;&lt;td&gt;&lt;code&gt;mdoc/core.py&lt;/code&gt;&lt;/td&gt;&lt;td&gt;纯逻辑，零依赖，可单测，唯一数据写入方&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CLI&lt;/td&gt;&lt;td&gt;&lt;code&gt;mdoc/cli.py&lt;/code&gt;&lt;/td&gt;&lt;td&gt;core 的薄壳，argparse + &lt;code&gt;--json&lt;/code&gt; 结构化输出&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;skill&lt;/td&gt;&lt;td&gt;LLM 前端&lt;/td&gt;&lt;td&gt;模型驱动 mdoc 命令，不直接碰文件&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;关键决策：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP 后置&lt;/strong&gt;——先让 core+CLI 落定，MCP 服务作为将来对多台机器的能力出口&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;doc.json&lt;/code&gt; / &lt;code&gt;patch.json&lt;/code&gt; 中间格式承载创建/更新&lt;/li&gt;
&lt;li&gt;写操作二次确认：&lt;code&gt;--dry-run&lt;/code&gt; 预览 + 用户确认保证&lt;/li&gt;
&lt;li&gt;零第三方依赖，stdlib 单测 119 个&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;阶段 1-2：core + CLI&lt;a href=&quot;#阶段-1-2core--cli&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;core 层实现了全部确定性逻辑：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;分类过滤（&lt;code&gt;type: reference&lt;/code&gt;，排除 user/feedback/project）&lt;/li&gt;
&lt;li&gt;kebab-case 文件名&lt;/li&gt;
&lt;li&gt;frontmatter 解析与 YAML 安全（含「冒号+空格」的字段自动加引号）&lt;/li&gt;
&lt;li&gt;INDEX/MEMORY 索引同步&lt;/li&gt;
&lt;li&gt;全文搜索（索引 + frontmatter + 正文，相关度 + 时间排序）&lt;/li&gt;
&lt;li&gt;validate 校验&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;CLI 提供 &lt;code&gt;init/config/list/search/get/create/update/delete/slugify/validate&lt;/code&gt; 命令，&lt;code&gt;--json&lt;/code&gt; 模式下 stdout 只输出一条 JSON 供 skill 消费。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;阶段 3：迁移现网 skill 命令协议&lt;a href=&quot;#阶段-3迁移现网-skill-命令协议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;把现网 skill 的 SKILL.md 里三处直接跑 python 脚本的逻辑替换为 mdoc 命令；创建/更新/删除流程改为 &lt;code&gt;mdoc create/update/delete&lt;/code&gt; + &lt;code&gt;--dry-run&lt;/code&gt; 确认。个人胶水内容（auto-memory 路径、node_type 兼容、博客发布规则等）保留不动。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;阶段 4：打包 + 通用 skill 模板 + 分发&lt;a href=&quot;#阶段-4打包--通用-skill-模板--分发&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;打包分发阶段做了四件事：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pyproject&lt;/strong&gt;：SPDX license = MIT、&lt;code&gt;package-data&lt;/code&gt; 收 skill_template、动态版本、入口 &lt;code&gt;mdoc = mdoc.cli:main&lt;/code&gt;，新增 LICENSE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用模板&lt;/strong&gt; &lt;code&gt;mdoc/skill_template/SKILL.md&lt;/code&gt;：自包含、零机器路径（不含任何个人机器路径）、命令协议全走 mdoc CLI，斜杠调用形式为空格分隔：&lt;/li&gt;
&lt;/ul&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/mdoc -f   # 按名字查文档&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/mdoc -l   # 列出文档&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/mdoc -c   # 创建&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/mdoc -u   # 更新&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/mdoc -d   # 删除&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;core.init_store&lt;/code&gt; 幂等写入模板到 &lt;code&gt;&amp;lt;store&amp;gt;/SKILL.md&lt;/code&gt;（返回 written|exists|absent）；&lt;code&gt;load_config&lt;/code&gt; 增加 cwd 向上发现 &lt;code&gt;.mdoc.toml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;构建 wheel + sdist，在干净 venv 里跑陌生机流程（init → 建库 → 搜索 → 删除）验收通过&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;0.1.1：任意目录可解析文档库（2026-08-10）&lt;a href=&quot;#011任意目录可解析文档库2026-08-10&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;重构完成后收到用户反馈：&lt;code&gt;pip --force-reinstall&lt;/code&gt; 重装后，&lt;code&gt;mdoc list&lt;/code&gt; 报「未配置文档库」，文档像”消失”了——但文件其实都在。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;根因&lt;/strong&gt;：文档库在 &lt;code&gt;&amp;lt;桌面&amp;gt;/mdoc&lt;/code&gt; 子目录。从桌面根目录运行 &lt;code&gt;mdoc list&lt;/code&gt; 时，store_dir 解析的 cwd 向上发现 &lt;code&gt;.mdoc.toml&lt;/code&gt;（桌面 → 更上层）&lt;strong&gt;不进入子目录&lt;/strong&gt;，又缺少用户级配置，于是 store_dir 为空，报「未配置」。文件从未被删除——唯一的文件删除路径是显式 &lt;code&gt;mdoc delete&lt;/code&gt;，重装没有任何钩子动数据。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;修复（v0.1.1）&lt;/strong&gt;：&lt;code&gt;mdoc init&lt;/code&gt; 新增 &lt;code&gt;_ensure_user_store_dir&lt;/code&gt;，把新建库注册进用户级配置的 &lt;code&gt;store_dir&lt;/code&gt; 字段：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用正斜杠规避 TOML 转义&lt;/li&gt;
&lt;li&gt;行级编辑，保留 &lt;code&gt;index_file&lt;/code&gt; / &lt;code&gt;[classification]&lt;/code&gt; / &lt;code&gt;[style]&lt;/code&gt; 等既有配置&lt;/li&gt;
&lt;li&gt;已有有效 store_dir 时不覆盖，防止劫持已有库&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;之后&lt;strong&gt;在任意目录&lt;/strong&gt;跑 mdoc 命令都能解析到库，无需先 cd 进库目录。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;重装安全&lt;/strong&gt;：&lt;code&gt;pip --force-reinstall&lt;/code&gt; 只更新程序本体，不改文档和配置；重跑 &lt;code&gt;mdoc init &amp;lt;库目录&amp;gt;&lt;/code&gt; 幂等，只补缺失文件。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;恢复方式&lt;/strong&gt;（如果遇到同样问题）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 方式一：重跑 init，幂等&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mdoc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;你的库目录&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 方式二：手动写入用户级配置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 在 ~/.mdoc.toml 写入（注意用正斜杠）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# store_dir = &quot;&amp;lt;你的库路径&amp;gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;验证&lt;/strong&gt;：119 个单测全绿（新增 4 个：用户配置写入 / 不覆盖已有库 / 保留其他键 / 库外任意目录 list 解析到库）；干净 venv 陌生机验收完整复刻用户场景——先报「未配置」，重跑 init 后任意目录 list 成功。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;验收踩坑（重要）&lt;a href=&quot;#验收踩坑重要&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Windows 编码 bug&lt;/strong&gt;：非 UTF-8 locale 下 &lt;code&gt;--stdin&lt;/code&gt; 读中文按 GBK + surrogateescape 解码成孤立代理对，写回 UTF-8 时报 &lt;code&gt;UnicodeEncodeError: surrogates not allowed&lt;/code&gt;。修复：在 &lt;code&gt;main()&lt;/code&gt; 里把 stdin/stdout/stderr 全部 &lt;code&gt;reconfigure(utf-8)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解析顺序 bug&lt;/strong&gt;（手工验收抓到）：在陌生库目录内跑 &lt;code&gt;mdoc config&lt;/code&gt; 解析到了用户配置里的默认库，把 create/delete 打到了错误位置。根因是 store_dir 解析时用户配置排在了 cwd 发现之前，与文档化的优先级链相悖。修复：cwd 发现的库优先于用户配置，并补了回归测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;教训&lt;/strong&gt;：陌生机验收如果用 &lt;code&gt;MDOC_CONFIG=nonexistent&lt;/code&gt; 隔离用户配置，反而会掩盖配置优先级顺序 bug——真实陌生机验收要保留用户配置在场。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;配置解析优先级（最终定稿）&lt;a href=&quot;#配置解析优先级最终定稿&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;store_dir 的解析优先级最终定为：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;--store &amp;gt; MDOC_DIR &amp;gt; 当前目录向上发现 .mdoc.toml（最多 5 层，到主目录即停）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt; 用户配置 ~/.mdoc.toml（$MDOC_CONFIG 可换）&amp;gt; 未配置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;选定库后，库本地配置 &lt;code&gt;&amp;lt;store&amp;gt;/.mdoc.toml&lt;/code&gt; 再覆盖 &lt;code&gt;index_file&lt;/code&gt; / 排除项 / 风格等；在库目录内运行命令会自动选中该库。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;未来：MCP 服务&lt;a href=&quot;#未来mcp-服务&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;重构的初衷之一是将来做成 MCP 服务，供多台机器使用。core+CLI 的分层为 MCP 预留了自然挂点：MCP tool 可以直接复用 core 函数（确定性），也可以包一层 CLI。当前决策是 MCP 后置，等 core+CLI 稳定后再评估。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这次重构把一套散落在 skill 里的”约定”变成了可测试、可分发、可复用的工具。核心收益有三个：确定性操作全部由代码保证，skill 只做 LLM 前端；零依赖的 core 让单测（119 个）和验收可以完整覆盖；0.1.1 的任意目录解析能力让安装后的使用体验真正脱离了”必须 cd 进库目录”的限制。项目开源在 &lt;a href=&quot;https://github.com/sanya2485/mdoc&quot; target=&quot;_blank&quot;&gt;github.com/sanya2485/mdoc&lt;/a&gt;。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>Coze 聊天窗 z-index 与 Swup 切页适配修复</title><link>https://www.sanyablog.cn/posts/coze-chat-theme-zindex/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/coze-chat-theme-zindex/</guid><description>Coze 聊天窗接入 Swup SPA 切页遇到的三个坑：z-index 层级被遮挡、切页后悬浮球消失、SDK 注入样式被 SwupHeadPlugin 清空</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;博客的 Coze AI 助手聊天窗接入 Swup 做 SPA 切页后，陆续踩了三个相互独立的坑：聊天窗被浮动控件遮挡、切页后悬浮球图标永久消失、聊天窗”打不开”其实是 SDK 注入的样式被切页清掉了。这篇文章记录这三个问题的根因和修法。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;z-index 层级&lt;a href=&quot;#z-index-层级&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;聊天窗默认 &lt;code&gt;ui.base.zIndex&lt;/code&gt; 是 1000，和博客的浮动控件（FloatingControls 1000 / FloatingTOC 999/1001）同级，会被遮住。把它提到 &lt;strong&gt;1020&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;聊天窗：&lt;code&gt;ui.base.zIndex = 1020&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;悬浮球：1010&lt;/li&gt;
&lt;li&gt;toast：9999&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;之后如果再被遮挡，按这张层级表往上调即可。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;Swup 切页重置（修复悬浮球图标消失）&lt;a href=&quot;#swup-切页重置修复悬浮球图标消失&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;根因&lt;/strong&gt;：悬浮球和 SDK 容器在 &lt;code&gt;#swup-container&lt;/code&gt; 之外，跨页存活；聊天窗开着时切页，&lt;code&gt;coze-chat-open&lt;/code&gt; class 残留 → 悬浮球永久消失。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;修法&lt;/strong&gt;：注册 &lt;code&gt;content:replace&lt;/code&gt; 回调，切页时收起聊天窗：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;swup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hooks&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;content:replace&quot;&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;classList&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;coze-chat-open&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cozeClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hideChatBot&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;注册时做兜底判断：&lt;code&gt;if (window.swup &amp;amp;&amp;amp; window.swup.hooks)&lt;/code&gt; 已就绪直接用，否则监听 &lt;code&gt;swup:enable&lt;/code&gt; 事件（布局层 dispatch）。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;SwupScriptsPlugin 重跑脚本坑&lt;a href=&quot;#swupscriptsplugin-重跑脚本坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;SwupScriptsPlugin 默认 scope 是整个 document，每次 &lt;code&gt;content:replace&lt;/code&gt;（&lt;strong&gt;含首次加载&lt;/strong&gt;）会重跑所有没带 &lt;code&gt;data-swup-ignore-script&lt;/code&gt; 的 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;。对 CozeChat 的破坏链：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;重跑 → 新 IIFE 里 &lt;code&gt;cozeClient&lt;/code&gt; 归零&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bindCozeWidget&lt;/code&gt; 把悬浮球重绑到空客户端&lt;/li&gt;
&lt;li&gt;重跑时 &lt;code&gt;readyState&lt;/code&gt; 已是 complete，走 &lt;code&gt;requestIdleCallback(initCozeChat, 500)&lt;/code&gt; 分支&lt;/li&gt;
&lt;li&gt;第二参传数字抛 &lt;code&gt;TypeError&lt;/code&gt;（&lt;code&gt;Argument 2 can&apos;t be converted to a dictionary&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initCozeChat&lt;/code&gt; 永不执行 → 点悬浮球只弹 toast，聊天窗永远打不开&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;内联脚本加 &lt;code&gt;data-swup-ignore-script&lt;/code&gt;，让浮窗独立于页面只初始化一次&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requestIdleCallback&lt;/code&gt; 第二参从数字改成 &lt;code&gt;{ timeout: 500 }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;通用教训&lt;/strong&gt;：任何 &lt;code&gt;is:inline&lt;/code&gt; 内联脚本如果声明了跨页状态（IIFE 内的 &lt;code&gt;var&lt;/code&gt;），都必须加 &lt;code&gt;data-swup-ignore-script&lt;/code&gt; 防止 Swup 重跑，否则状态会被第二次执行清零。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;SwupHeadPlugin 清 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 坑&lt;a href=&quot;#swupheadplugin-清-style-坑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Coze SDK 初始化时会往 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 动态注入约 230 个 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;（聊天窗 &lt;code&gt;position:fixed&lt;/code&gt;、&lt;code&gt;.coz-layout&lt;/code&gt; 布局、&lt;code&gt;.light-theme&lt;/code&gt; 主题变量、哈希类名组件样式）。之前为了做切页动画启用了 SwupHeadPlugin（&lt;code&gt;updateHead: true&lt;/code&gt;），它每次切页用新页面 head 替换当前 head → SDK 注入的运行时样式全被删（实测 headStyleCount 237→5）→ 聊天窗失去 &lt;code&gt;position:fixed&lt;/code&gt;、退化成 &lt;code&gt;static&lt;/code&gt; 跑到左上角。表现就是”切页后打不开”，其实 &lt;code&gt;cozeClient&lt;/code&gt; 活着、&lt;code&gt;showChatBot()&lt;/code&gt; 也执行了，只是样式没了。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;修法&lt;/strong&gt;：&lt;code&gt;updateHead&lt;/code&gt; 从 &lt;code&gt;true&lt;/code&gt; 改成：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;updateHead&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;persistTags&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;style&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这是 SwupHeadPlugin 官方选项，让所有内联 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 跨页存活。博客自身页面特异样式都是 scoped（&lt;code&gt;data-astro-cid-*&lt;/code&gt; / &lt;code&gt;svelte-*&lt;/code&gt;），保留无副作用。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;补充认知&lt;/strong&gt;：SDK 注入的样式里只有 129 个含 “coze”、其中 18 个不含 “coze-chat-sdk” 标记（面板定位是哈希类名规则）→ &lt;strong&gt;按内容字符串匹配不可靠&lt;/strong&gt;，别用 &lt;code&gt;textContent.includes&lt;/code&gt; 当 persistTags 判断条件。&lt;code&gt;persistTags&lt;/code&gt; 支持 boolean / CSS 选择器字符串 / 函数三种形式，能用字符串选择器就别用函数。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;实现位置&lt;a href=&quot;#实现位置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/components/widget/CozeChat.astro&lt;/code&gt;：&lt;code&gt;resetCozeOnNavigate()&lt;/code&gt;（切页收起聊天窗）+ &lt;code&gt;ui.base.zIndex: 1020&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt;：&lt;code&gt;updateHead: { persistTags: &quot;style&quot; }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这两个坑（脚本重跑 / head 样式被清）是两条独立破坏路径，之前只修了脚本重跑，没挡住样式被清——排查”切页后组件失灵”类问题时，建议先从这两个维度各自验证一遍。&lt;/p&gt;&lt;p&gt;关联阅读：&lt;a href=&quot;/posts/coze-widget-ui-customization/&quot;&gt;Coze 浮窗 UI 定制实战：从 SDK 配置到自建悬浮球&lt;/a&gt;&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>Coze 浮窗 UI 定制实战：从 SDK 配置到自建悬浮球</title><link>https://www.sanyablog.cn/posts/coze-widget-ui-customization/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/coze-widget-ui-customization/</guid><description>Coze Web SDK 1.2.0-beta.19 浮窗 UI 定制要点（语言、标题、footer、悬浮球、z-index），以及 CozeChat.astro 自建悬浮球与问候气泡的完整改造实录</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;博客的 Coze AI 助手用自建悬浮球 + 问候气泡替换了 SDK 默认的入口，全程逆向 Web SDK 1.2.0-beta.19（cn build）确认配置项行为。这篇文章整理 SDK 浮窗 UI 定制要点和改造中踩的坑。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;SDK 浮窗 UI 定制要点（逆向确认）&lt;a href=&quot;#sdk-浮窗-ui-定制要点逆向确认&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;英文 UI 根源&lt;/strong&gt;：&lt;code&gt;ui.base.lang&lt;/code&gt; 默认 &lt;code&gt;&quot;en&quot;&lt;/code&gt;，不设置就是英文界面，设 &lt;code&gt;&quot;zh-CN&quot;&lt;/code&gt; 全转中文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;header 标题&lt;/strong&gt;：默认硬编码 &lt;code&gt;&quot;Coze Bot&quot;&lt;/code&gt;，用 &lt;code&gt;ui.header.title&lt;/code&gt; 覆盖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;footer 链接&lt;/strong&gt;：默认「由 扣子 提供支持，AI生成仅供参考」，「扣子」是链接，用 &lt;code&gt;ui.footer.isShow=false&lt;/code&gt; 或 &lt;code&gt;ui.footer.expressionText&lt;/code&gt; 控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;悬浮球 asstBtn&lt;/strong&gt;：只有 &lt;code&gt;{ isNeed }&lt;/code&gt; 一个配置项，图标走 &lt;code&gt;ui.base.icon&lt;/code&gt;，class 是 hash 过的 CSS module → &lt;strong&gt;靠 CSS 覆写样式不可靠，要自定义样式就 &lt;code&gt;isNeed:false&lt;/code&gt; + 自建&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编程控制&lt;/strong&gt;：client 暴露 &lt;code&gt;showChatBot()&lt;/code&gt; / &lt;code&gt;hideChatBot()&lt;/code&gt;；生命周期回调 &lt;code&gt;ui.chatBot.onShow/onHide/onBeforeShow/onBeforeHide&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ui.chatBot.el&lt;/code&gt;&lt;/strong&gt;：可把聊天窗 portal 到自定义容器；顶层 &lt;code&gt;el&lt;/code&gt; 会把整个 app（球+窗）放进容器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ui.base.zIndex&lt;/code&gt;&lt;/strong&gt; 默认 1000，聊天窗与悬浮球共用 → 和博客浮动控件同级，需要提层（相关方案见 &lt;a href=&quot;/posts/coze-chat-theme-zindex/&quot;&gt;Coze 聊天窗 z-index 与 Swup 切页适配修复&lt;/a&gt;）。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;自建悬浮球与问候气泡&lt;a href=&quot;#自建悬浮球与问候气泡&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;悬浮球&lt;a href=&quot;#悬浮球&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;自建悬浮球 &lt;code&gt;#coze-ball&lt;/code&gt; 固定在右下角 56px，静止时 &lt;code&gt;translateY(28px)&lt;/code&gt; 半隐藏（下半截压出视口），hover 弹出 &lt;code&gt;translateY(-16px)&lt;/code&gt; 带回弹（&lt;code&gt;cubic-bezier(0.34,1.56,0.64,1)&lt;/code&gt;）。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;hover 抖动坑（已修）&lt;/strong&gt;：hover 判定绝不能绑在会移动的元素上。原来 &lt;code&gt;.coze-ball:hover&lt;/code&gt; 触发 &lt;code&gt;transform&lt;/code&gt;，动画途中球带着判定框上移，鼠标位置不变就脱离 &lt;code&gt;:hover&lt;/code&gt; → 弹出/缩回循环抖动。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;修法&lt;/strong&gt;：加一个不移动的 &lt;code&gt;.coze-hitbox&lt;/code&gt;（56×40，锚定右下角，&lt;code&gt;pointer-events:auto&lt;/code&gt;），hover 用 &lt;code&gt;.coze-hitbox:hover .coze-ball&lt;/code&gt;，球在框内纯视觉位移；点击也绑 hitbox（球是其子元素会冒泡）。移动端 hitbox 48×32。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;问候气泡&lt;a href=&quot;#问候气泡&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;问候气泡 &lt;code&gt;#coze-bubble&lt;/code&gt; 文案「hi~请问有什么问题吗？」，每次打开都弹；× 仅本次关闭 / 3s 自动关；气泡内「不再显示」按钮写 &lt;code&gt;coze_greeting_off&lt;/code&gt; 永久停用（此前是 &lt;code&gt;coze_greeting_seen&lt;/code&gt;”仅首次”语义，已改）。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：&lt;code&gt;clearCozeSessionCache()&lt;/code&gt; 会清掉所有 &lt;code&gt;coze_&lt;/code&gt;/&lt;code&gt;COZE_&lt;/code&gt; 前缀的 key（只留 &lt;code&gt;coze_visitor_id&lt;/code&gt;）。新加的持久化标记必须进白名单，否则每次刷新被清。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;问候气泡最终样式（模板 09）&lt;a href=&quot;#问候气泡最终样式模板-09&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;定位在悬浮球&lt;strong&gt;左上方&lt;/strong&gt;：桌面 &lt;code&gt;right:32px; bottom:84px&lt;/code&gt;，移动端 &lt;code&gt;right:24px; bottom:40px&lt;/code&gt;；尾巴 &lt;code&gt;right:20px; bottom:-8px&lt;/code&gt;（14px 方块 45°）指向球（移动端 &lt;code&gt;right:14px&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式&lt;/strong&gt;：圆体字（&lt;code&gt;&quot;MiSans&quot;,&quot;HarmonyOS Sans SC&quot;,&quot;PingFang SC&quot;,&quot;Microsoft YaHei&quot;&lt;/code&gt;）+ 20px 大圆角。&lt;strong&gt;颜色跟随主题色相&lt;/strong&gt;（改 &lt;code&gt;--hue&lt;/code&gt; 会同步变）：用 &lt;code&gt;color-mix(in oklab, var(--primary) 10%, var(--card-bg))&lt;/code&gt; 调淡主题色底、&lt;code&gt;primary 30% + --line-divider&lt;/code&gt; 描边、&lt;code&gt;primary 16% transparent&lt;/code&gt; 阴影；文字亮色 &lt;code&gt;--deep-text&lt;/code&gt;、暗色 &lt;code&gt;html.dark&lt;/code&gt; 覆盖为 &lt;code&gt;--btn-content&lt;/code&gt;。不支持 &lt;code&gt;color-mix&lt;/code&gt; 时降级 &lt;code&gt;--btn-regular-bg&lt;/code&gt;/&lt;code&gt;--line-divider&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坍缩 bug（重要）&lt;/strong&gt;：容器 &lt;code&gt;.coze-widget&lt;/code&gt; 是 &lt;code&gt;width:0&lt;/code&gt;，气泡仅设 &lt;code&gt;right&lt;/code&gt; 无显式宽度 → 绝对定位子元素走 shrink-to-fit，可用宽度为 0 → 坍缩成约 58px 窄条（“字体难看”的根因）。&lt;strong&gt;修法&lt;/strong&gt;：气泡加 &lt;code&gt;width:max-content; max-width:230px&lt;/code&gt;（显式非 auto 宽度避开 shrink-to-fit 分支）。移动端 &lt;code&gt;max-width:190px&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;气泡显示期间球要弹出&lt;/strong&gt;：&lt;code&gt;.coze-widget.coze-has-bubble .coze-ball { transform: translateY(-16px) }&lt;/code&gt;（否则球保持 28px 半隐，气泡尾巴会悬空 56px）。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;如何应用&lt;a href=&quot;#如何应用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;改 Coze 浮窗样式/行为时：先查上面的 SDK 配置表，别去覆写 SDK 的 hash class；需要自定义交互就 &lt;code&gt;asstBtn.isNeed:false&lt;/code&gt; 自建，再调 &lt;code&gt;showChatBot()&lt;/code&gt;。聊天窗被遮挡或切页失灵的问题，看 &lt;a href=&quot;/posts/coze-chat-theme-zindex/&quot;&gt;Coze 聊天窗 z-index 与 Swup 切页适配修复&lt;/a&gt;。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>一次阿里云 ECS 异地登录误报排查与顺手加固</title><link>https://www.sanyablog.cn/posts/aliyun-ecs-login-alert-investigation/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/aliyun-ecs-login-alert-investigation/</guid><description>凌晨收到阿里云异地登录告警，美国 IP 成功 SSH 登录 8 次——虚惊一场的背后是一次完整的安全排查与加固</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;凌晨的告警&lt;a href=&quot;#凌晨的告警&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;某天凌晨，阿里云发来异地登录告警：来自 &lt;strong&gt;美国弗吉尼亚&lt;/strong&gt; 的 IP 以 root 身份 SSH 登录了 &lt;strong&gt;8 次&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;第一反应：密钥泄露了。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;排查过程&lt;a href=&quot;#排查过程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;登录日志分析&lt;a href=&quot;#登录日志分析&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 追踪特定 IP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;xx.xx.xx.xx&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/var/log/secure&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 爆破统计&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;Failed password&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/var/log/secure&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;awk&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;{print $(NF-3)}&apos;&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;uniq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-c&lt;/span&gt;&lt;span&gt; | &lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-rn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 最近登录记录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;last&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;发现模式重复&lt;a href=&quot;#发现模式重复&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;为了验证，手动触发了一次 CI/CD 部署，服务器再次出现完全相同的连接模式：&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;/th&gt;&lt;th&gt;第一次（被报警）&lt;/th&gt;&lt;th&gt;手动触发&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;连接 IP&lt;/td&gt;&lt;td&gt;美国 Azure&lt;/td&gt;&lt;td&gt;美国 Azure&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;连接次数&lt;/td&gt;&lt;td&gt;8 次&lt;/td&gt;&lt;td&gt;8 次&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;连接模式&lt;/td&gt;&lt;td&gt;22 秒内多端口并发&lt;/td&gt;&lt;td&gt;22 秒内多端口并发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;部署结果&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;成功 ✅&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;模式完全一致&lt;/strong&gt;——原来是 GitHub Actions 的 runner 跑在 Azure 基础设施上，SCP 上传时建立多个 SSH 并发连接（分片传输），被阿里云异地登录检测误报了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;顺手发现真实攻击&lt;a href=&quot;#顺手发现真实攻击&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在同一份日志中发现了来自另一个 IP 的 &lt;strong&gt;287 次 SSH 密码爆破尝试&lt;/strong&gt;，持续 14 分钟，全部失败。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 封禁爆破 IP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;iptables&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-A&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;INPUT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-s&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xx.xx.xx.xx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-j&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;安全加固清单&lt;a href=&quot;#安全加固清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;趁这个机会完成了一次完整的安全加固：&lt;/p&gt;
































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;措施&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;关闭 SSH 密码登录&lt;/td&gt;&lt;td&gt;&lt;code&gt;PasswordAuthentication no&lt;/code&gt;，只允许密钥登录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;安装 fail2ban&lt;/td&gt;&lt;td&gt;5 次失败自动封禁 24 小时&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;更换 SSH 密钥&lt;/td&gt;&lt;td&gt;旧密钥作废，更换新密钥对&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;更新 CI/CD Secrets&lt;/td&gt;&lt;td&gt;替换为新私钥&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;封禁爆破 IP&lt;/td&gt;&lt;td&gt;iptables DROP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;解封 CI/CD IP&lt;/td&gt;&lt;td&gt;确认为 GitHub Actions，从黑名单移除&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;section&gt;&lt;h3&gt;后门排查&lt;a href=&quot;#后门排查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;确认是误报后，还是彻底检查了一遍系统：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;当前运行进程 → 无异常&lt;/li&gt;
&lt;li&gt;systemd 服务 → 无新增服务&lt;/li&gt;
&lt;li&gt;crontab / 定时任务 → 无后门&lt;/li&gt;
&lt;li&gt;系统用户 → 只有必要的系统用户&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authorized_keys&lt;/code&gt; → 清理到只剩 2 个必要密钥&lt;/li&gt;
&lt;li&gt;Docker 容器 → 无异常&lt;/li&gt;
&lt;li&gt;系统文件完整性（&lt;code&gt;rpm -Va&lt;/code&gt;）→ 无篡改&lt;/li&gt;
&lt;li&gt;监听端口 → 仅 22(SSH) + 80(nginx)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt;、&lt;code&gt;/dev/shm&lt;/code&gt; → 无可疑文件&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;虚惊一场——所谓的”攻击”是 GitHub Actions CI/CD 的正常 SCP 上传行为，因 runner 位于美国 Azure 区域被阿里云异地登录检测误报。但借着这次误报完成了一次系统安全加固，也算是有惊无险的收获。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：如果你也用 GitHub Actions 部署到国内云服务器，收到异地登录告警时先确认是不是 CI runner 的连接，别急着封 IP。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>用 Subagent 构建博客发布流水线：归档、审查、发布一条龙</title><link>https://www.sanyablog.cn/posts/blog-subagent-workflow-setup/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/blog-subagent-workflow-setup/</guid><description>为博客维护工作流创建 3 个 subagent（mdoc-archiver / doc-validator / blog-publisher）和 blog-pipeline skill，实现从代码改动到博客发布的完全自动化</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;日常维护博客时，常常需要反复执行”代码改动 -&amp;gt; 归档到 /mdoc -&amp;gt; 写成博客 -&amp;gt; 发布”这一流程。手动操作不仅繁琐，而且容易遗漏步骤——有时候修完 bug 就直接切去干别的事了，改完的配置、排查的思路全忘了归档。更烦人的是，单个 agent 处理这么长的链条，上下文一长就容易出现幻觉。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我基于 Claude Code 的 subagent 机制搭了一套三步流水线，把归档、审查、发布全自动化了。&lt;/p&gt;
&lt;section&gt;&lt;h2&gt;两层架构：Skill 编排 + Subagent 执行&lt;a href=&quot;#两层架构skill-编排--subagent-执行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;系统分为两层，各司其职：&lt;/p&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;组件&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;th&gt;存放位置&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;编排层&lt;/td&gt;&lt;td&gt;&lt;code&gt;blog-pipeline&lt;/code&gt; skill&lt;/td&gt;&lt;td&gt;检测修复场景、调度 subagent、展示用户提示&lt;/td&gt;&lt;td&gt;&lt;code&gt;~/.claude/skills/blog-pipeline/SKILL.md&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;执行层&lt;/td&gt;&lt;td&gt;&lt;code&gt;mdoc-archiver&lt;/code&gt; subagent&lt;/td&gt;&lt;td&gt;将代码改动归档到 /mdoc 文档&lt;/td&gt;&lt;td&gt;&lt;code&gt;~/.claude/agents/mdoc-archiver.md&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;执行层&lt;/td&gt;&lt;td&gt;&lt;code&gt;doc-validator&lt;/code&gt; subagent&lt;/td&gt;&lt;td&gt;审查文档准确性，防幻觉&lt;/td&gt;&lt;td&gt;&lt;code&gt;~/.claude/agents/doc-validator.md&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;执行层&lt;/td&gt;&lt;td&gt;&lt;code&gt;blog-publisher&lt;/code&gt; subagent&lt;/td&gt;&lt;td&gt;将 /mdoc 文档转为博客并发布&lt;/td&gt;&lt;td&gt;&lt;code&gt;~/.claude/agents/blog-publisher.md&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;每个 subagent 都有独立的 context window，互不干扰。skill 层只负责编排调度，不参与具体的文档处理。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;blog-pipeline Skill：编排入口&lt;a href=&quot;#blog-pipeline-skill编排入口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;blog-pipeline skill 是整个工作流的调度大脑，承担四项职责：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动检测修复场景&lt;/strong&gt; — 判断当前对话是否包含代码改动、问题排查、修复完成等信号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轻量用户提示&lt;/strong&gt; — 检测到修复场景时在回复末尾显示三段式提示&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三步流水线执行&lt;/strong&gt; — 按顺序调用三个 subagent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全规则&lt;/strong&gt; — push 前必须展示 git diff 并等待用户确认&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;当工作流检测到修复场景时，会在回复末尾显示这样的提示：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[r] 一条龙（归档→审查→博客→push）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[a] 仅归档到 /mdoc&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[v] 审查博客与 /mdoc 源文档一致性&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[s] 跳过&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;无侵入设计：未检测到修复场景时完全不打扰用户。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三步流水线&lt;a href=&quot;#三步流水线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;完整的流程如下图：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;代码改动/修复完成&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[blog-pipeline skill 检测到修复场景]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── 用户选 [r] ──→ Step 1: mdoc-archiver 归档到 /mdoc&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                   Step 2: doc-validator 审查文档&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ┌─────┴─────┐&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ▼           ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                  通过         不通过 → 用户决定修正/跳过/取消&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                   Step 3: blog-publisher 写博客&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                   git diff 展示给用户确认&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                         │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ┌─────┴─────┐&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ▼           ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                  确认         拒绝 → 不 push，保留本地&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                    ▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│                   git commit + git push → GitHub Actions 自动部署&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── 用户选 [a] ──→ 仅执行 Step 1（归档）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── 用户选 [v] ──→ 审查博客与 /mdoc 一致性&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── [s] 跳过 ──→ 什么也不做&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;关键设计原则&lt;a href=&quot;#关键设计原则&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每步独立 context window&lt;/strong&gt; — 每个 subagent 有独立的上下文，减少长上下文带来的幻觉。归档时不用关心博客格式，写博客时不用关心代码细节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;doc-validator 只读&lt;/strong&gt; — 审查者与写文档者分离，交叉验证控制幻觉。validator 只有 Read/Grep/Glob 三个工具，没有写入权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;push 前确认&lt;/strong&gt; — git diff 必须展示给用户，防止误推送。这是最重要的安全防线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无侵入提示&lt;/strong&gt; — 未检测到修复场景时完全不打扰用户，避免无效干扰&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;三个 Subagent 的职责分工&lt;a href=&quot;#三个-subagent-的职责分工&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Subagent&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;th&gt;工具&lt;/th&gt;&lt;th&gt;模型&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;mdoc-archiver&lt;/td&gt;&lt;td&gt;分析代码改动/修复方案，提取结构化信息（问题描述、根因、解决方案、涉及文件、命令），按 /mdoc 格式写入 memory 目录。会先 Glob 检查是否已有同名文档，决定新建还是补充更新&lt;/td&gt;&lt;td&gt;Read, Grep, Glob, Bash, Write&lt;/td&gt;&lt;td&gt;sonnet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;doc-validator&lt;/td&gt;&lt;td&gt;纯只读审查：文件路径真实存在、命令合理、代码引用与实际一致、跨文档链接有效。输出审查结论：通过 / 有警告 / 不通过&lt;/td&gt;&lt;td&gt;Read, Grep, Glob&lt;/td&gt;&lt;td&gt;sonnet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;blog-publisher&lt;/td&gt;&lt;td&gt;将 /mdoc 文档转为 Astro 博客文章，写入 &lt;code&gt;src/content/posts/&lt;/code&gt;。确保 frontmatter 完整，适配博客风格（概述、小标题、代码块标注语言），删除内部文档专用字段。创建后显示 git diff 待用户确认&lt;/td&gt;&lt;td&gt;Read, Grep, Glob, Bash, Write&lt;/td&gt;&lt;td&gt;sonnet&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;section&gt;&lt;h3&gt;mdoc-archiver 设计要点&lt;a href=&quot;#mdoc-archiver-设计要点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;从代码改动中提取结构化信息，包括问题描述、根因分析、解决方案、涉及的文件和命令。写入前会用 Glob 搜索是否已有相关文档，如果有则询问覆盖还是追加。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;doc-validator 设计要点&lt;a href=&quot;#doc-validator-设计要点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;纯只读，工具限 Read / Grep / Glob。职责：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新文档审查&lt;/strong&gt; — 验证路径存在、命令合理、代码引用一致、链接有效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性审查&lt;/strong&gt;（&lt;code&gt;[v]&lt;/code&gt;）— 比对本地文档与博客文章，确保内容同步、无内部字段泄漏。遵循时间线一致性规则——以最新文档为准，旧文档允许过时&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;blog-publisher 设计要点&lt;a href=&quot;#blog-publisher-设计要点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;将 /mdoc 文档转化为博客文章时做三件事：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;确保 frontmatter 完整（title、published、tags、category、draft）&lt;/li&gt;
&lt;li&gt;适配博客风格：开头有概述段落，分节用小标题，代码块标注语言&lt;/li&gt;
&lt;li&gt;删除内部文档专用的 frontmatter 字段（name、description、metadata）&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;特殊情况处理&lt;a href=&quot;#特殊情况处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;情况&lt;/th&gt;&lt;th&gt;处理方式&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;无可归档内容&lt;/td&gt;&lt;td&gt;提示用户，问是否手动提供&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;文档已存在&lt;/td&gt;&lt;td&gt;问覆盖还是追加，再执行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;审查不通过&lt;/td&gt;&lt;td&gt;展示问题，让用户决定修正/跳过/取消&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;push 前发现不对&lt;/td&gt;&lt;td&gt;不确认即可，保留本地不推送&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;使用方式&lt;a href=&quot;#使用方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;自动流程（推荐）&lt;a href=&quot;#自动流程推荐&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;日常修复完代码后，blog-pipeline skill 会自动检测到修复场景，在回复末尾显示轻量提示：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;输入 &lt;code&gt;r&lt;/code&gt; — 自动走完归档→审查→博客→push 完整流程&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;a&lt;/code&gt; — 仅归档到 /mdoc&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;v&lt;/code&gt; — 审查博客与 /mdoc 源文档一致性&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;s&lt;/code&gt; — 跳过&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;手动触发&lt;a href=&quot;#手动触发&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;也可以直接对 skill 或 subagent 下达指令：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;“用 mdoc-archiver 把这次修复归档” — 归档到 /mdoc&lt;/li&gt;
&lt;li&gt;“用 doc-validator 审查刚才那篇文档” — 交叉验证&lt;/li&gt;
&lt;li&gt;“用 blog-publisher 把 nginx 配置那篇写成博客发出去” — 发布&lt;/li&gt;
&lt;li&gt;“执行 blog-pipeline” — 触发完整流水线&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;变更内容&lt;a href=&quot;#变更内容&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;无代码变更。创建了 1 个 skill 定义文件和 3 个 subagent 定义文件：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.claude/skills/blog-pipeline/SKILL.md&lt;/code&gt; — 编排层，包含修复场景检测、用户提示模板、三步流水线调度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/agents/mdoc-archiver.md&lt;/code&gt; — 归档 subagent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/agents/doc-validator.md&lt;/code&gt; — 审查 subagent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/agents/blog-publisher.md&lt;/code&gt; — 发布 subagent&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;注意：subagent 仅存放在用户级目录 &lt;code&gt;~/.claude/agents/&lt;/code&gt;，项目级目录 &lt;code&gt;~/Desktop/myFireflyBlog/.claude/agents/&lt;/code&gt; 目前留空，仅供项目配置预留。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这套流水线的核心思路是&lt;strong&gt;分治 + 交叉验证&lt;/strong&gt;：把长流程拆成三个独立步骤，每步由专门的 subagent 处理，中间加一层只读审查来兜底幻觉。从实际使用来看，doc-validator 确实抓出过几次归档文档里的路径错误和命令拼写问题，起到了预期的把关作用。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>使用 GitHub Actions + Docker 自动化部署 Astro 静态博客</title><link>https://www.sanyablog.cn/posts/deploy-astro-blog-with-github-actions/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/deploy-astro-blog-with-github-actions/</guid><description>从手动 SCP 上传到一键 CI/CD 部署，记录 myFireflyBlog 的自动化部署方案演进</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;动机&lt;a href=&quot;#动机&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;项目使用 Astro 构建生成 &lt;code&gt;dist/&lt;/code&gt; 静态文件，之前一直是手动执行 &lt;code&gt;pnpm build&lt;/code&gt; → SCP 上传 → SSH 重启容器。每次更新都要重复这套流程，效率低且容易出错。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;方案设计&lt;a href=&quot;#方案设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;采用 &lt;strong&gt;GitHub Actions + Docker + nginx&lt;/strong&gt; 的自动化部署方案：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Git Push → GitHub Actions (构建) → SCP → Server (nginx:alpine)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;为什么不用 docker-compose？&lt;a href=&quot;#为什么不用-docker-compose&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;生产环境只有一个 nginx 容器，没有后端 API、数据库或其他服务，单容器的场景上 docker-compose 是过度工程。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;为什么构建不在服务器上做？&lt;a href=&quot;#为什么构建不在服务器上做&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;构建在 GitHub Actions 的 CI runner 上完成，服务器只做文件宿主 + 运行时启动。服务器不需要装 pnpm、Node.js，也不需要跑构建，攻击面更小、运维更轻。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;工作流设计&lt;a href=&quot;#工作流设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;.github/workflows/deploy-server.yml&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;触发条件：push 到 master 分支&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;步骤：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;1. Checkout 代码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;2. Setup Node.js 22 + pnpm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;3. pnpm install --frozen-lockfile&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;4. pnpm build&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;5. SCP 上传 dist/ 到服务器&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;6. SSH 执行 docker run nginx:alpine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;Secrets 配置&lt;a href=&quot;#secrets-配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Secret&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SERVER_HOST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;服务器 IP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SERVER_USERNAME&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SSH 用户名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SERVER_SSH_KEY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SSH 私钥&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;BLOG_DOMAIN&lt;/code&gt;&lt;/td&gt;&lt;td&gt;博客域名或 IP（nginx 模板用）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;容器运行&lt;a href=&quot;#容器运行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;服务器上直接跑 &lt;code&gt;nginx:alpine&lt;/code&gt; 官方镜像，通过 &lt;code&gt;-v&lt;/code&gt; 挂载静态文件目录：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myFireflyBlog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/blog:/usr/share/nginx/html:ro&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;80:80&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--restart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nginx:alpine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;Nginx 配置&lt;a href=&quot;#nginx-配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;通过独立的 &lt;code&gt;deploy-nginx.yml&lt;/code&gt; workflow 管理，与博客内容部署解耦：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;Gzip 压缩&lt;a href=&quot;#gzip-压缩&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_types &lt;/span&gt;&lt;span&gt;text/plain text/css application/json application/javascript image/svg+xml;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;gzip_vary &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;安全头&lt;a href=&quot;#安全头&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;X-Content-Type-Options &lt;/span&gt;&lt;span&gt;&quot;nosniff&quot;&lt;/span&gt;&lt;span&gt; always;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;X-Frame-Options &lt;/span&gt;&lt;span&gt;&quot;DENY&quot;&lt;/span&gt;&lt;span&gt; always;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;X-XSS-Protection &lt;/span&gt;&lt;span&gt;&quot;1; mode=block&quot;&lt;/span&gt;&lt;span&gt; always;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;Referrer-Policy &lt;/span&gt;&lt;span&gt;&quot;strict-origin-when-cross-origin&quot;&lt;/span&gt;&lt;span&gt; always;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;缓存策略（针对 Astro 构建产物）&lt;a href=&quot;#缓存策略针对-astro-构建产物&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 内容哈希文件名 —— 不可变缓存&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /_astro/   {&lt;/span&gt;&lt;span&gt; expires &lt;/span&gt;&lt;span&gt;365d&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;public, immutable&quot;&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 通用静态资源&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /assets/   {&lt;/span&gt;&lt;span&gt; expires &lt;/span&gt;&lt;span&gt;30d&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt; add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;public&quot;&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 搜索索引&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /pagefind/ {&lt;/span&gt;&lt;span&gt; expires &lt;/span&gt;&lt;span&gt;7d&lt;/span&gt;&lt;span&gt;;  &lt;/span&gt;&lt;span&gt; add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;public&quot;&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# HTML 页面不缓存（内容更新需即时生效）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; ~* &lt;/span&gt;&lt;span&gt;\.html$ &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt; expires &lt;/span&gt;&lt;span&gt;-1;  &lt;/span&gt;&lt;span&gt; add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;no-cache&quot;&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;一些经验&lt;a href=&quot;#一些经验&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;条件挂载避免耦合&lt;a href=&quot;#条件挂载避免耦合&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;nginx 配置通过独立 workflow 管理，博客内容 deploy 时检测配置是否存在：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CONFIG_MOUNT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; /path/to/conf/default.conf ]; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;CONFIG_MOUNT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;-v /path/to/conf/default.conf:/etc/nginx/conf.d/default.conf:ro&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myFireflyBlog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/dist:/usr/share/nginx/html:ro&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;$CONFIG_MOUNT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;80:80&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--restart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nginx:alpine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;配置存在则挂载，不存在则退化到默认配置，两个 workflow 独立但不耦合。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;安全加固顺手做&lt;a href=&quot;#安全加固顺手做&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在配置部署的过程中顺带加固了 SSH 安全：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;关闭密码登录（&lt;code&gt;PasswordAuthentication no&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;安装 fail2ban 防爆破&lt;/li&gt;
&lt;li&gt;更换密钥对&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;总结&lt;a href=&quot;#总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这套方案跑下来最满意的一点是&lt;strong&gt;变更域分离&lt;/strong&gt;——博客内容和服务器配置走不同的 workflow，各自独立更新互不影响。改配置不需要重新构建，发文章不需要碰服务器配置。&lt;/p&gt;&lt;p&gt;对比之前的纯手动流程，现在每天更新博客的心理负担降到了零：写完 &lt;code&gt;git push&lt;/code&gt; 等三分钟就好了。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>隐藏的博客文章还在首页显示？排查 Astro draft 机制问题</title><link>https://www.sanyablog.cn/posts/draft-post-not-showing-fix/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/draft-post-not-showing-fix/</guid><description>排查 Astro 博客中 draft: true 文章仍显示预览和分类计数错误的经历，以及 HTML 缓存策略的优化</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;问题现象&lt;a href=&quot;#问题现象&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;把博客里的示例文章全部设为 &lt;code&gt;draft: true&lt;/code&gt; 后，出现了两个问题：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;首页仍能看见隐藏文章的预览卡片&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分类（“文章示例&quot;&quot;博客指南”）后面的文章数字还是旧的&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;排查过程&lt;a href=&quot;#排查过程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;第一层：draft 过滤逻辑&lt;a href=&quot;#第一层draft-过滤逻辑&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;查看 &lt;code&gt;src/utils/content-utils.ts&lt;/code&gt; 中获取文章列表的函数：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allBlogPosts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;posts&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, ({ &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PROD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;问题出在这里。&lt;code&gt;import.meta.env.PROD&lt;/code&gt; 在本地开发环境（&lt;code&gt;pnpm dev&lt;/code&gt;）下是 &lt;code&gt;false&lt;/code&gt;，此时过滤函数恒返回 &lt;code&gt;true&lt;/code&gt;——&lt;strong&gt;所有文章包括草稿全部显示&lt;/strong&gt;。所以本地预览时永远看到全部文章，分类计数也是错的。&lt;/p&gt;&lt;p&gt;修复很简单：去掉环境判断，始终过滤草稿。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;第二层：为什么部署到服务器后还不对？&lt;a href=&quot;#第二层为什么部署到服务器后还不对&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;代码改完、部署成功后，用户说还是看到旧内容。这就不是 draft 逻辑的问题了——应该是浏览器缓存。&lt;/p&gt;&lt;p&gt;检查服务器返回的响应头：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Cache-Control: no-cache&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;no-cache&lt;/code&gt; 的语义是”可以缓存，但使用前必须向服务器验证”。大部分浏览器会遵守这个规则，但 Safari 等浏览器的 &lt;strong&gt;bfcache（后退缓存）&lt;/strong&gt; 在回退导航时会直接使用页面快照，根本不发验证请求。&lt;/p&gt;&lt;p&gt;将 HTML 的缓存策略改为更严格的 &lt;code&gt;no-store&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 旧&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; ~* &lt;/span&gt;&lt;span&gt;\.html$ &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;expires &lt;/span&gt;&lt;span&gt;-1;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;no-cache&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 新&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; ~* &lt;/span&gt;&lt;span&gt;\.html$ &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;add_header &lt;/span&gt;&lt;span&gt;Cache-Control &lt;/span&gt;&lt;span&gt;&quot;no-store, must-revalidate&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;no-store&lt;/code&gt; 告诉浏览器&lt;strong&gt;完全不要缓存页面&lt;/strong&gt;，每次导航都从服务器获取最新内容。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;第三层：配置怎么每次部署都丢了？&lt;a href=&quot;#第三层配置怎么每次部署都丢了&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;配置写好后，部署一次博客——配置又丢了，容器退回到默认 nginx 配置。&lt;/p&gt;&lt;p&gt;查 &lt;code&gt;deploy-server.yml&lt;/code&gt; 的 SCP 步骤：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;dist/&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/path/to/blog&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;rm&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;rm: true&lt;/code&gt; 每次部署&lt;strong&gt;清空整个目标目录&lt;/strong&gt;再上传。而我把 nginx 配置放在了 &lt;code&gt;/path/to/blog/conf/&lt;/code&gt; 下——正好被清掉。&lt;/p&gt;&lt;p&gt;修复：把 nginx 配置文件移出 SCP 的打击范围。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/path/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── blog/          # SCP 目标（rm:true 清空这里）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── dist/      # 博客静态文件&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── nginx/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── conf/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── default.conf   # nginx 配置（SCP 碰不到）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;经验总结&lt;a href=&quot;#经验总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;问题&lt;/th&gt;&lt;th&gt;教训&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;draft 过滤&lt;/td&gt;&lt;td&gt;&lt;code&gt;import.meta.env.PROD&lt;/code&gt; 的判断会让草稿在开发环境可见，计数也受影响&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HTML 缓存&lt;/td&gt;&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt; 不如 &lt;code&gt;no-store&lt;/code&gt; 严格，bfcache 会绕过验证&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;配置文件被删&lt;/td&gt;&lt;td&gt;SCP 的 &lt;code&gt;rm: true&lt;/code&gt; 清空整个目标目录，配置必须放在外面&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;三个问题单独看都不复杂，但串在一起会导致”明明部署成功了，为什么还是旧内容”的困惑——每个环节都以为自己没做错，合起来效果就不对。&lt;/p&gt;&lt;/section&gt;</content:encoded></item><item><title>Nginx 配置与项目部署解耦：envsubst + 独立 Workflow</title><link>https://www.sanyablog.cn/posts/nginx-config-separation-practice/</link><guid isPermaLink="true">https://www.sanyablog.cn/posts/nginx-config-separation-practice/</guid><description>解决 nginx 配置和博客内容耦合在同一个 CI/CD 流程中的问题，实现各自独立更新</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h2&gt;问题&lt;a href=&quot;#问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;博客的 nginx 一直跑着官方默认配置——Gzip 没开、安全头没有、缓存策略全无。更麻烦的是，nginx 配置和博客内容耦合在同一个部署流程里：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;deploy-server.yml: pnpm build → SCP dist/ → docker run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;想改个缓存时间？得改项目代码、走完整构建、等 CI/CD 跑完。改完容器重启，配置又丢了（无状态容器）。&lt;/p&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;方案：四层解耦&lt;a href=&quot;#方案四层解耦&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;1. 模板 + envsubst&lt;a href=&quot;#1-模板--envsubst&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;仓库是公开的，敏感信息（域名/IP）不能明文存。用 &lt;code&gt;envsubst&lt;/code&gt; 做模板变量替换：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;server/nginx/default.conf.template&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;server_name &lt;/span&gt;&lt;span&gt; ${&lt;/span&gt;&lt;span&gt;BLOG_DOMAIN&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;CI/CD 中替换后上传：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;envsubst &apos;${BLOG_DOMAIN}&apos; &amp;lt; template &amp;gt; default.conf&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;envsubst &apos;${BLOG_DOMAIN}&apos;&lt;/code&gt; 精确替换指定变量，nginx 自身的 &lt;code&gt;$&lt;/code&gt; 变量（如 &lt;code&gt;$remote_addr&lt;/code&gt;）不受影响。&lt;/p&gt;&lt;p&gt;GitHub Secrets 里存 &lt;code&gt;BLOG_DOMAIN&lt;/code&gt;，仓库里只有模板，不留明文。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 独立 Workflow&lt;a href=&quot;#2-独立-workflow&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;新增 &lt;code&gt;.github/workflows/deploy-nginx.yml&lt;/code&gt;，只做一件事：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;手动触发 → envsubst → SCP default.conf → nginx -t 测试 → nginx -s reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;和博客内容部署完全独立：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;改 nginx 配置 → 编辑模板 → 手动触发 Deploy Nginx Config → 10 秒生效&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;改博客内容 → git push → Deploy to Server 自动触发 → 3 分钟&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 条件挂载&lt;a href=&quot;#3-条件挂载&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;博客部署时检测配置文件是否存在：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CONFIG_MOUNT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; /path/to/conf/default.conf ]; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;CONFIG_MOUNT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;-v /path/to/conf/default.conf:/etc/nginx/conf.d/default.conf:ro&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;myFireflyBlog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/dist:/usr/share/nginx/html:ro&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;$CONFIG_MOUNT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;80:80&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--restart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nginx:alpine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;配置存在则挂载，不存在则退化到默认配置——&lt;strong&gt;零耦合故障点&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 配置与内容目录分离&lt;a href=&quot;#4-配置与内容目录分离&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;服务器上配置文件和静态文件分开放：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;/path/to/blog/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── dist/           # 博客静态文件（SCP 上传）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── conf/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── default.conf  # nginx 配置（独立 workflow 管理）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;两个目录互不覆盖，内容 deploy 不会丢配置，配置更新不影响内容。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h2&gt;结果&lt;a href=&quot;#结果&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;/th&gt;&lt;th&gt;之前&lt;/th&gt;&lt;th&gt;之后&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;改缓存策略&lt;/td&gt;&lt;td&gt;改代码 → push → 等 3 分钟构建&lt;/td&gt;&lt;td&gt;改模板 → 触发 workflow → 10 秒 reload&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;改博客内容&lt;/td&gt;&lt;td&gt;同上流程&lt;/td&gt;&lt;td&gt;git push → 自动部署（不影响配置）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;回滚配置&lt;/td&gt;&lt;td&gt;无解&lt;/td&gt;&lt;td&gt;git revert → 触发 workflow&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;服务器重建&lt;/td&gt;&lt;td&gt;手配 nginx&lt;/td&gt;&lt;td&gt;跑 workflow 一键恢复&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;最直接的收益：&lt;strong&gt;改 nginx 配置从”半小时的项目发布”变成了”10 秒的运维操作”&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;</content:encoded></item></channel></rss>